diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e435f4e --- /dev/null +++ b/.editorconfig @@ -0,0 +1,27 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{java,kt}] +ij_kotlin_imports_layout = * + +# Disable wildcard imports +ij_kotlin_name_count_to_use_star_import = 999 +ij_kotlin_name_count_to_use_star_import_for_members = 999 +ij_java_class_count_to_use_import_on_demand = 999 + +ktlint_code_style = android_studio + +# Disable trailing comma +ktlint_standard_trailing-comma-on-call-site = disabled +ktlint_standard_trailing-comma-on-declaration-site = disabled + +max_line_length = off + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..411c077 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto eol=lf + +*.bat text eol=crlf +*.jar binary diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..62eb927 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +open_collective: tusky diff --git a/.github/ISSUE_TEMPLATE/1.bug_report.yml b/.github/ISSUE_TEMPLATE/1.bug_report.yml new file mode 100644 index 0000000..7295570 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1.bug_report.yml @@ -0,0 +1,41 @@ +name: Bug Report +description: If something isn't working as expected +labels: [bug] +body: + - type: markdown + attributes: + value: | + Make sure that you are submitting a new bug that was not previously reported or already fixed. + + Please use a concise and distinct title for the issue. + + If possible, attach screenshots, videos or links to posts to illustrate the problem. + - type: textarea + attributes: + label: Detailed description + validations: + required: false + - type: textarea + attributes: + label: Steps to reproduce the problem + description: What were you trying to do? + value: | + 1. + 2. + 3. + ... + validations: + required: false + - type: textarea + attributes: + label: Debug information + description: | + This info can be copied from the 'About' screen in Tusky 24+. + If you are on a lower version or can't access the screen, please provide us with the Tusky Version, Android Version, Device and the Mastodon instance this problem occurred on. + placeholder: | + Tusky Test 22.0-b814c2c0 + Android 12 + Fairphone 4 + mastodon.social + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/2.feature_request.yml b/.github/ISSUE_TEMPLATE/2.feature_request.yml new file mode 100644 index 0000000..8f66f49 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2.feature_request.yml @@ -0,0 +1,19 @@ +name: Feature Request +description: I have a suggestion +labels: [enhancement] +body: + - type: markdown + attributes: + value: Please use a concise and distinct title for the issue. + - type: textarea + attributes: + label: Pitch + description: Describe your idea for a feature. Make sure it has not already been suggested/implemented/turned down before. + validations: + required: true + - type: textarea + attributes: + label: Motivation + description: Why do you think this feature is needed? Who would benefit from it? + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0086358 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: true diff --git a/.github/ci-gradle.properties b/.github/ci-gradle.properties new file mode 100644 index 0000000..b27d297 --- /dev/null +++ b/.github/ci-gradle.properties @@ -0,0 +1,24 @@ +# +# Copyright 2023 Tusky Contributors +# +# This file is a part of Tusky. +# +# This program is free software; you can redistribute it and/or modify it under the terms of the +# GNU General Public License as published by the Free Software Foundation; either version 3 of the +# License, or (at your option) any later version. +# +# Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even +# the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General +# Public License for more details. +# +# You should have received a copy of the GNU General Public License along with Tusky; if not, +# see <http://www.gnu.org/licenses>. +# + +# CI build workers are ephemeral, so don't benefit from the Gradle daemon +org.gradle.daemon=false +org.gradle.parallel=true +org.gradle.workers.max=2 + +kotlin.incremental=false +kotlin.compiler.execution.strategy=in-process diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..479c523 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,44 @@ +name: CI + +on: + push: + tags: + - '*' + pull_request: + workflow_dispatch: + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Gradle Wrapper Validation + uses: gradle/actions/wrapper-validation@v3 + + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - name: Gradle Build Action + uses: gradle/gradle-build-action@v3 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} + + - name: ktlint + run: ./gradlew clean ktlintCheck + + - name: Regular lint + run: ./gradlew app:lintGreenDebug + + - name: Test + run: ./gradlew app:testGreenDebugUnitTest + + - name: Build + run: ./gradlew app:buildGreenDebug diff --git a/.github/workflows/populate-gradle-build-cache.yml b/.github/workflows/populate-gradle-build-cache.yml new file mode 100644 index 0000000..0207cce --- /dev/null +++ b/.github/workflows/populate-gradle-build-cache.yml @@ -0,0 +1,34 @@ +# Build the app on each push to `develop`, populating the build cache to speed +# up CI on PRs. + +name: Populate build cache + +on: + push: + branches: + - develop + +jobs: + build: + name: app:buildGreenDebug + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Gradle Wrapper Validation + uses: gradle/actions/wrapper-validation@v3 + + - name: Copy CI gradle.properties + run: mkdir -p ~/.gradle ; cp .github/ci-gradle.properties ~/.gradle/gradle.properties + + - uses: gradle/gradle-build-action@v3 + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} + + - name: Run app:buildGreenDebug + run: ./gradlew app:buildGreenDebug diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..67ab69c --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.iml +.gradle +/local.properties +/.idea +.DS_Store +build +/captures +.externalNativeBuild +app/release +app-release.apk +.kotlin diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ab13d8e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,291 @@ +# Tusky changelog + +## Unreleased or Tusky Nightly + +### New features and other improvements + +### Significant bug fixes + +## v25.2 + +### Significant bug fixes + +- Fixes a bug that could sometimes crash Tusky when rotating the screen while viewing an account list [PR#4430](https://github.com/tuskyapp/Tusky/pull/4430) +- Fixes a bug that could crash Tusky at startup under certain conditions [PR#4431](https://github.com/tuskyapp/Tusky/pull/4431) +- Fixes a bug that caused Tusky to crash when custom emojis with too large dimensions were loaded [PR#4429](https://github.com/tuskyapp/Tusky/pull/4429) +- Makes Tusky work again with Iceshrimp by working around a quirk in their API implementation [PR#4426](https://github.com/tuskyapp/Tusky/pull/4426) +- Fixes a bug that made translations not work on some servers [PR#4422](https://github.com/tuskyapp/Tusky/pull/4422) + +## v25.1 + +### Significant bug fixes + +- Fixed two crashes at startup introduced in 25.0 [PR#4415](https://github.com/tuskyapp/Tusky/pull/4415) [PR#4417](https://github.com/tuskyapp/Tusky/pull/4417) + +## v25.0 + +### New features and other improvements + +- Added support for the [Mastodon translation api](https://docs.joinmastodon.org/methods/statuses/#translate). + You can now find a new option "translate" in the three-dot-menu on posts that are not in your display language when your server supports the translation api. + Support is determined by checking the `configuration.translation.enabled` attribute of the `/api/v2/instance` endpoint. + [PR#4307](https://github.com/tuskyapp/Tusky/pull/4307) +- The language of a post is now shown in the metadata section of the detail post view, if it is available. [PR#4127](https://github.com/tuskyapp/Tusky/pull/4127) +- The transitions between screens have been changed to feel faster and align more with default Android transitions. [PR#4285](https://github.com/tuskyapp/Tusky/pull/4285) +- The post statistic section below the detail post view is now always shown to prevent layout shifts on the first like or boost. + [PR#4205](https://github.com/tuskyapp/Tusky/pull/4205) [PR#4260](https://github.com/tuskyapp/Tusky/pull/4260) +- The filters for boosts/replies/self-boosts in the home timeline have moved from general preferences to account specific preferences. [PR#4115](https://github.com/tuskyapp/Tusky/pull/4115) +- The json parsing library has been migrated from Gson to Moshi. This change will make Tusky no longer crash on unexpected server responses. [PR#4309](https://github.com/tuskyapp/Tusky/pull/4309) +- Small layout improvements to the header of the profile view [PR#4375](https://github.com/tuskyapp/Tusky/pull/4375) [PR#4371](https://github.com/tuskyapp/Tusky/pull/4371) +- support for Android 14 Upside Down Cake [PR#4224](https://github.com/tuskyapp/Tusky/pull/4224) +- Various internal refactorings to improve performance and maintainability. + [PR#4269](https://github.com/tuskyapp/Tusky/pull/4269) + [PR#4290](https://github.com/tuskyapp/Tusky/pull/4290) + [PR#4291](https://github.com/tuskyapp/Tusky/pull/4291) + [PR#4296](https://github.com/tuskyapp/Tusky/pull/4296) + [PR#4364](https://github.com/tuskyapp/Tusky/pull/4364) + [PR#4366](https://github.com/tuskyapp/Tusky/pull/4366) + [PR#4372](https://github.com/tuskyapp/Tusky/pull/4372) + [PR#4356](https://github.com/tuskyapp/Tusky/pull/4356) + [PR#4348](https://github.com/tuskyapp/Tusky/pull/4348) + [PR#4339](https://github.com/tuskyapp/Tusky/pull/4339) + [PR#4337](https://github.com/tuskyapp/Tusky/pull/4337) + [PR#4336](https://github.com/tuskyapp/Tusky/pull/4336) + [PR#4330](https://github.com/tuskyapp/Tusky/pull/4330) + [PR#4235](https://github.com/tuskyapp/Tusky/pull/4235) + [PR#4081](https://github.com/tuskyapp/Tusky/pull/4081) + +### Significant bug fixes + +- The setting to hide the notification filter bar that was accidentally removed is back. [PR#4225](https://github.com/tuskyapp/Tusky/pull/4225) +- The profile picture in the bottom navigation bar now has the correct content description. [PR#4400](https://github.com/tuskyapp/Tusky/pull/4400) + +## v24.1 + +- The screen will stay on again while a video is playing. [PR#4168](https://github.com/tuskyapp/Tusky/pull/4168) +- A memory leak has been fixed. This should improve stability and performance. [PR#4150](https://github.com/tuskyapp/Tusky/pull/4150) [PR#4153](https://github.com/tuskyapp/Tusky/pull/4153) +- Emojis are now correctly counted as 1 character when composing a post. [PR#4152](https://github.com/tuskyapp/Tusky/pull/4152) +- Fixed a crash when text was selected on some devices. [PR#4166](https://github.com/tuskyapp/Tusky/pull/4166) +- The icons in the help texts of empty timelines will now always be correctly + aligned. [PR#4179](https://github.com/tuskyapp/Tusky/pull/4179) +- Fixed ANR caused by direct message badge [PR#4182](https://github.com/tuskyapp/Tusky/pull/4182) + +## v24.0 + +### New features and other improvements + +- The number of tabs that can be configured is no longer limited. [PR#4058](https://github.com/tuskyapp/Tusky/pull/4058) +- Blockquotes and code blocks in posts now look nicer [PR#4090](https://github.com/tuskyapp/Tusky/pull/4090) [PR#4091](https://github.com/tuskyapp/Tusky/pull/4091) +- The old behavior of the notification tab (pre Tusky 22.0) has been restored. [PR#4015](https://github.com/tuskyapp/Tusky/pull/4015) +- Role badges are now shown on profiles (Mastodon 4.2 feature). [PR#4029](https://github.com/tuskyapp/Tusky/pull/4029) +- The video player has been upgraded to Google Jetpack Media3; video compatibility should be improved, and you can now adjust playback speed. [PR#3857](https://github.com/tuskyapp/Tusky/pull/3857) +- New theme option to use the black theme when following the system design. [PR#3957](https://github.com/tuskyapp/Tusky/pull/3957) +- Following the system design is now the default theme setting. [PR#3813](https://github.com/tuskyapp/Tusky/pull/3957) +- A new view to see trending posts is available both in the menu and as custom tab. [PR#4007](https://github.com/tuskyapp/Tusky/pull/4007) +- A new option to hide self boosts has been added. [PR#4101](https://github.com/tuskyapp/Tusky/pull/4101) +- The `api/v2/instance` endpoint is now supported. [PR#4062](https://github.com/tuskyapp/Tusky/pull/4062) +- New settings for lists: + - Hide from the home timeline [PR#3932](https://github.com/tuskyapp/Tusky/pull/3932) + - Decide which replies should be shown in the list [PR#4072](https://github.com/tuskyapp/Tusky/pull/4072) +- The oldest supported Android version is now Android 7 Nougat [PR#4014](https://github.com/tuskyapp/Tusky/pull/4014) + +### Significant bug fixes + +- **Empty trends no longer causes Tusky to crash**, [PR#3853](https://github.com/tuskyapp/Tusky/pull/3853) + + +## v23.0 + +### New features and other improvements + +- **New preference to scale UI text**, [PR#3248](https://github.com/tuskyapp/Tusky/pull/3248) by [@nikclayton](https://mastodon.social/@nikclayton) + +### Significant bug fixes + +- **Save account information correctly**, [PR#3720](https://github.com/tuskyapp/Tusky/pull/3720) by [@connyduck](https://chaos.social/@ConnyDuck) + - If you were logged in with multiple accounts it was possible to switch accounts in a way that the UI showed the new account, but database operations were happening using the old account. +- **"pull" notifications on devices running Android versions <= 11**, [PR#3649](https://github.com/tuskyapp/Tusky/pull/3649) by [@nikclayton](https://mastodon.social/@nikclayton) + - Pull notifications (i.e., not using ntfy.sh) could silently fail on devices running Android 11 and below +- **Work around Android bug where text fields could "forget" they can copy/paste**, [PR#3707](https://github.com/tuskyapp/Tusky/pull/3707) by [@nikclayton](https://mastodon.social/@nikclayton) +- **Viewing "diffs" in edit history will not extend off screen edge**, [PR#3431](https://github.com/tuskyapp/Tusky/pull/3431) by [@nikclayton](https://mastodon.social/@nikclayton) +- **Don't crash if your server has no post edit history**, [PR#3747](https://github.com/tuskyapp/Tusky/pull/3747) by [@nikclayton](https://mastodon.social/@nikclayton) + - Your Mastodon server might know that a post has been edited, but not know the details of those edits. Trying to view the history of those statuses no longer crashes. +- **Add a "Delete" button when editing a filter**, [PR#3553](https://github.com/tuskyapp/Tusky/pull/3553) by [@Tak](https://mastodon.gamedev.place/@Tak) +- **Show non-square emoji correctly**, [PR#3711](https://github.com/tuskyapp/Tusky/pull/3711) by [@connyduck](https://chaos.social/@ConnyDuck) +- **Potential crash when editing profile fields**, [PR#3808](https://github.com/tuskyapp/Tusky/pull/3808) by [@nikclayton](https://mastodon.social/@nikclayton) +- **Oversized context menu when editing image descriptions**, [PR#3787](https://github.com/tuskyapp/Tusky/pull/3787) by [@connyduck](https://chaos.social/@ConnyDuck) + +## v23.0 beta 2 + +### Significant bug fixes + +- **Potential crash when editing profile fields**, [PR#3808](https://github.com/tuskyapp/Tusky/pull/3808) by [@nikclayton](https://mastodon.social/@nikclayton) +- **Oversized context menu when editing image descriptions**, [PR#3787](https://github.com/tuskyapp/Tusky/pull/3787) by [@connyduck](https://chaos.social/@ConnyDuck) + +## v23.0 beta 1 + +### New features and other improvements + +- **New preference to scale UI text**, [PR#3248](https://github.com/tuskyapp/Tusky/pull/3248) by [@nikclayton](https://mastodon.social/@nikclayton) + +### Significant bug fixes + +- **Save account information correctly**, [PR#3720](https://github.com/tuskyapp/Tusky/pull/3720) by [@connyduck](https://chaos.social/@ConnyDuck) + - If you were logged in with multiple accounts it was possible to switch accounts in a way that the UI showed the new account, but database operations were happening using the old account. +- **"pull" notifications on devices running Android versions <= 11**, [PR#3649](https://github.com/tuskyapp/Tusky/pull/3649) by [@nikclayton](https://mastodon.social/@nikclayton) + - Pull notifications (i.e., not using ntfy.sh) could silently fail on devices running Android 11 and below +- **Work around Android bug where text fields could "forget" they can copy/paste**, [PR#3707](https://github.com/tuskyapp/Tusky/pull/3707) by [@nikclayton](https://mastodon.social/@nikclayton) +- **Viewing "diffs" in edit history will not extend off screen edge**, [PR#3431](https://github.com/tuskyapp/Tusky/pull/3431) by [@nikclayton](https://mastodon.social/@nikclayton) +- **Don't crash if your server has no post edit history**, [PR#3747](https://github.com/tuskyapp/Tusky/pull/3747) by [@nikclayton](https://mastodon.social/@nikclayton) + - Your Mastodon server might know that a post has been edited, but not know the details of those edits. Trying to view the history of those statuses no longer crashes. +- **Add a "Delete" button when editing a filter**, [PR#3553](https://github.com/tuskyapp/Tusky/pull/3553) by [@Tak](https://mastodon.gamedev.place/@Tak) +- **Show non-square emoji correctly**, [PR#3711](https://github.com/tuskyapp/Tusky/pull/3711) by [@connyduck](https://chaos.social/@ConnyDuck) + +## v22.0 + +### New features and other improvements + +- **View trending hashtags**, [PR#3149](https://github.com/tuskyapp/Tusky/pull/3149) by [@knossos](https://fosstodon.org/@knossos) + - View trending hashtags from the side menu, or by adding them to a new tab. +- **Edit image description and focus point**, [PR#3215](https://github.com/tuskyapp/Tusky/pull/3215) by [@Tak](https://mastodon.gamedev.place/@Tak) + - Edit image descriptions and focus points when editing posts. +- **View profile banner images**, [PR#3274](https://github.com/tuskyapp/Tusky/pull/3274) by [@Tak](https://mastodon.gamedev.place/@Tak) + - Tap the banner image on any profile to view it full size, save, share, etc. +- **Follow new hashtags**, [PR#3275](https://github.com/tuskyapp/Tusky/pull/3275) by [@nikclayton](https://mastodon.social/@nikclayton) + - Follow new hashtags from the "Followed hashtags" screen. +- **Better ordering when selecting languages**, [PR#3293](https://github.com/tuskyapp/Tusky/pull/3293) by [@Tak](https://mastodon.gamedev.place/@Tak) + - Tusky will prioritise the language of the post being replied to, your default posting language, configured Tusky languages, and configured system languages when ordering the list of languages to post in. +- **"Load more" break is more prominent**, [PR#3376](https://github.com/tuskyapp/Tusky/pull/3376) by [@lakoja](https://freiburg.social/@lakoja) + - Adjusted the design so the "Load more" break in a timeline is more obvious. +- **Add "Refresh" menu**, [PR#3121](https://github.com/tuskyapp/Tusky/pull/3121) by [@nikclayton](https://mastodon.social/@nikclayton) + - Tusky timelines can now be refreshed from a menu as well as swiping, making this accessible to assistive devices. +- **Notifications timeline improvements**, [PR#3159](https://github.com/tuskyapp/Tusky/pull/3159) by [@nikclayton](https://mastodon.social/@nikclayton) + - Notifications no longer need to "Load more", they are loaded automatically as you scroll. + - Errors when interacting with notifications are displayed to the user, with a "Retry" option. +- **Show the difference between versions of a post**, [PR#3314](https://github.com/tuskyapp/Tusky/pull/3314) by [@nikclayton](https://mastodon.social/@nikclayton) + - Viewing the edits to a post highlights the differences (text that was added or deleted) between the different versions. +- **Support Mastodon v4 filters**, [PR#3188](https://github.com/tuskyapp/Tusky/pull/3188) by [@Tak](https://mastodon.gamedev.place/@Tak) + - Mastodon v4 introduced additional [filtering controls](https://docs.joinmastodon.org/user/moderating/#filters). +- **Option to show post statistics in the timeline**, [PR#3413](https://github.com/tuskyapp/Tusky/pull/3413) + - Tusky can now (optionally) show the number of replies, reposts, and favourites a post has received, in the timeline. +- **Expanded tappable area for links, hashtags, and mentions in a post**, [PR#3382](https://github.com/tuskyapp/Tusky/pull/3382) by [@nikclayton](https://mastodon.social/@nikclayton) + - Links, hashtags, and mentions in a post now react to taps that are a little above, below, or to the side of the tappable text, making them more accessible. + +### Significant bug fixes + +- **Remember selected tab and position**, [PR#3255](https://github.com/tuskyapp/Tusky/pull/3255) by [@nikclayton](https://mastodon.social/@nikclayton) + - Changing your tab settings (adding, removing, re-ordering) remembers your reading position in those tabs. +- **Show player controls during audio playback**, [PR#3286](https://github.com/tuskyapp/Tusky/pull/3286) by [@EricFrohnhoefer](https://mastodon.social/@EricFrohnhoefer) + - A regression from v21.0 where the media player controls could not be used. +- **Keep notifications until read**, [PR#3312](https://github.com/tuskyapp/Tusky/pull/3312) by [@lakoja](https://freiburg.social/@lakoja) + - Opening Tusky would dismiss all active Tusky Android notifications. +- **Fix copying URLs at the end of a post**, [PR#3380](https://github.com/tuskyapp/Tusky/pull/3380) by [@nikclayton](https://mastodon.social/@nikclayton) + - Copying a URL from the end of a post could include an extra Unicode whitespace character, making the URL unusable as is. +- **Correctly display mixed RTL and LTR text in profiles**, [PR#3328](https://github.com/tuskyapp/Tusky/pull/3328) by [@nikclayton](https://mastodon.social/@nikclayton) + - Profile text that contained a mix of right-to-left and left-to-right writing directions would display incorrectly. +- **Stop showing duplicates of edited posts in threads**, [PR#3377](https://github.com/tuskyapp/Tusky/pull/3377) by [@Tak](https://mastodon.gamedev.place/@Tak) + - Editing a post in thread view would show the old and new version of the post in the thread. +- **Correct post length calculation**, [PR#3392](https://github.com/tuskyapp/Tusky/pull/3392) by [@nikclayton](https://mastodon.social/@nikclayton) + - In a post that mentioned a user (e.g., `@tusky@mastodon.social`) Tusky was incorrectly including the `@mastodon.social` part when calculating the post's length, leading to incorrect "This post is too long" errors. +- **Always publish image captions**, [PR#3421](https://github.com/tuskyapp/Tusky/pull/3421) by [@lakoja](https://freiburg.social/@lakoja) + - Finishing editing an image caption before the image had finished loading would lose the caption. +- **Clicking "Compose" from a notification would set the wrong account**, [PR#3688](https://github.com/tuskyapp/Tusky/pull/3688) + +## v22.0 beta 7 + +### Significant bug fixes + +- **Fetch all outstanding Mastodon notifications when creating Android notifications**, [PR#3700](https://github.com/tuskyapp/Tusky/pull/3700) +- **Clicking "Compose" from a notification would set the wrong account**, [PR#3688](https://github.com/tuskyapp/Tusky/pull/3688) +- **Ensure "last read notification ID" is saved to the correct account**, [PR#3697](https://github.com/tuskyapp/Tusky/pull/3697) + +## v22.0 beta 6 + +### Significant bug fixes + +- **Save reading position in the Notifications tab more frequently**, [PR#3685](https://github.com/tuskyapp/Tusky/pull/3685) + +## v22.0 beta 5 + +## Significant bug fixes + +- **Rolled back APNG library to fix broken animated emojis**, [PR#3676](https://github.com/tuskyapp/Tusky/pull/3676) +- **Save local copy of notification marker in case server does not support the API**, [PR#3672](https://github.com/tuskyapp/Tusky/pull/3672) + +## v22.0 beta 4 + +### Significant bug fixes + +- **Fixed repeated fetch of notifications if configured with multiple accounts**, [PR#3660](https://github.com/tuskyapp/Tusky/pull/3660) + +## v22.0 beta 3 + +### Significant bug fixes + +- **Fixed crash when viewing a thread**, [PR#3622](https://github.com/tuskyapp/Tusky/pull/3622) +- **Fixed crash processing Mastodon filters**, [PR#3634](https://github.com/tuskyapp/Tusky/pull/3634) +- **Links in bios of follow/follow request notifications are clickable**, [PR#3646](https://github.com/tuskyapp/Tusky/pull/3646) +- **Android Notifications updates**, [PR#3636](https://github.com/tuskyapp/Tusky/pull/3626) + - Android notification for a Mastodon notification should only be shown once + - Android notifications are grouped by Mastodon notification type (follow, mention, boost, etc) + - Potential for missing notifications has been removed + +## v22.0 beta 2 + +### Significant bug fixes + +- **Improved notification loading speed**, [PR#3598](https://github.com/tuskyapp/Tusky/pull/3598) +- **Restore showing 0/1/1+ for replies**, [PR#3590](https://github.com/tuskyapp/Tusky/pull/3590) +- **Show filter titles, not filter keywords, on filtered posts**, [PR#3589](https://github.com/tuskyapp/Tusky/pull/3589) +- **Fixed a bug where opening a status could open an unrelated link**, [PR#3600](https://github.com/tuskyapp/Tusky/pull/3600) +- **Show "Add" button in correct place when there are no filters**, [PR#3561](https://github.com/tuskyapp/Tusky/pull/3561) +- **Fixed assorted crashes** + +## v22.0 beta 1 + +### New features and other improvements + +- **View trending hashtags**, [PR#3149](https://github.com/tuskyapp/Tusky/pull/3149) by [@knossos](https://fosstodon.org/@knossos) + - View trending hashtags from the side menu, or by adding them to a new tab. +- **Edit image description and focus point**, [PR#3215](https://github.com/tuskyapp/Tusky/pull/3215) by [@Tak](https://mastodon.gamedev.place/@Tak) + - Edit image descriptions and focus points when editing posts. +- **View profile banner images**, [PR#3274](https://github.com/tuskyapp/Tusky/pull/3274) by [@Tak](https://mastodon.gamedev.place/@Tak) + - Tap the banner image on any profile to view it full size, save, share, etc. +- **Follow new hashtags**, [PR#3275](https://github.com/tuskyapp/Tusky/pull/3275) by [@nikclayton](https://mastodon.social/@nikclayton) + - Follow new hashtags from the "Followed hashtags" screen. +- **Better ordering when selecting languages**, [PR#3293](https://github.com/tuskyapp/Tusky/pull/3293) by [@Tak](https://mastodon.gamedev.place/@Tak) + - Tusky will prioritise the language of the post being replied to, your default posting language, configured Tusky languages, and configured system languages when ordering the list of languages to post in. +- **"Load more" break is more prominent**, [PR#3376](https://github.com/tuskyapp/Tusky/pull/3376) by [@lakoja](https://freiburg.social/@lakoja) + - Adjusted the design so the "Load more" break in a timeline is more obvious. +- **Add "Refresh" menu**, [PR#3121](https://github.com/tuskyapp/Tusky/pull/3121) by [@nikclayton](https://mastodon.social/@nikclayton) + - Tusky timelines can now be refreshed from a menu as well as swiping, making this accessible to assistive devices. +- **Notifications timeline improvements**, [PR#3159](https://github.com/tuskyapp/Tusky/pull/3159) by [@nikclayton](https://mastodon.social/@nikclayton) + - Notifications no longer need to "Load more", they are loaded automatically as you scroll. + - Errors when interacting with notifications are displayed to the user, with a "Retry" option. +- **Show the difference between versions of a post**, [PR#3314](https://github.com/tuskyapp/Tusky/pull/3314) by [@nikclayton](https://mastodon.social/@nikclayton) + - Viewing the edits to a post highlights the differences (text that was added or deleted) between the different versions. +- **Support Mastodon v4 filters**, [PR#3188](https://github.com/tuskyapp/Tusky/pull/3188) by [@Tak](https://mastodon.gamedev.place/@Tak) + - Mastodon v4 introduced additional [filtering controls](https://docs.joinmastodon.org/user/moderating/#filters). +- **Option to show post statistics in the timeline**, [PR#3413](https://github.com/tuskyapp/Tusky/pull/3413) + - Tusky can now (optionally) show the number of replies, reposts, and favourites a post has received, in the timeline. +- **Expanded tappable area for links, hashtags, and mentions in a post**, [PR#3382](https://github.com/tuskyapp/Tusky/pull/3382) by [@nikclayton](https://mastodon.social/@nikclayton) + - Links, hashtags, and mentions in a post now react to taps that are a little above, below, or to the side of the tappable text, making them more accessible. + +### Significant bug fixes + +- **Remember selected tab and position**, [PR#3255](https://github.com/tuskyapp/Tusky/pull/3255) by [@nikclayton](https://mastodon.social/@nikclayton) + - Changing your tab settings (adding, removing, re-ordering) remembers your reading position in those tabs. +- **Show player controls during audio playback**, [PR#3286](https://github.com/tuskyapp/Tusky/pull/3286) by [@EricFrohnhoefer](https://mastodon.social/@EricFrohnhoefer) + - A regression from v21.0 where the media player controls could not be used. +- **Keep notifications until read**, [PR#3312](https://github.com/tuskyapp/Tusky/pull/3312) by [@lakoja](https://freiburg.social/@lakoja) + - Opening Tusky would dismiss all active Tusky Android notifications. +- **Fix copying URLs at the end of a post**, [PR#3380](https://github.com/tuskyapp/Tusky/pull/3380) by [@nikclayton](https://mastodon.social/@nikclayton) + - Copying a URL from the end of a post could include an extra Unicode whitespace character, making the URL unusable as is. +- **Correctly display mixed RTL and LTR text in profiles**, [PR#3328](https://github.com/tuskyapp/Tusky/pull/3328) by [@nikclayton](https://mastodon.social/@nikclayton) + - Profile text that contained a mix of right-to-left and left-to-right writing directions would display incorrectly. +- **Stop showing duplicates of edited posts in threads**, [PR#3377](https://github.com/tuskyapp/Tusky/pull/3377) by [@Tak](https://mastodon.gamedev.place/@Tak) + - Editing a post in thread view would show the old and new version of the post in the thread. +- **Correct post length calculation**, [PR#3392](https://github.com/tuskyapp/Tusky/pull/3392) by [@nikclayton](https://mastodon.social/@nikclayton) + - In a post that mentioned a user (e.g., `@tusky@mastodon.social`) Tusky was incorrectly including the `@mastodon.social` part when calculating the post's length, leading to incorrect "This post is too long" errors. +- **Always publish image captions**, [PR#3421](https://github.com/tuskyapp/Tusky/pull/3421) by [@lakoja](https://freiburg.social/@lakoja) + - Finishing editing an image caption before the image had finished loading would lose the caption. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..0bf4d3a --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,57 @@ +# Contributing + +Thanks for your interest in contributing to Tusky! Here are some informations to help you get started. + +If you have any questions, don't hesitate to open an issue or join our [development chat on Matrix](https://riot.im/app/#/room/#Tusky:matrix.org). + +## Contributing translations + +Translations are managed on our [Weblate](https://weblate.tusky.app/projects/tusky/tusky/). You can create an account and translate texts through the interface, no coding knowledge required. +To add a new language, click on the 'Start a new translation' button on at the bottom of the page. + +- Use gender-neutral language +- Address users informally (e.g. in German "du" and never "Sie") + +## Contributing code + +### Prerequisites +You should have a general understanding of Android development and Git. + +### Architecture +We try to follow the [Guide to app architecture](https://developer.android.com/topic/architecture). + +### Kotlin +Tusky was originally written in Java, but is in the process of migrating to Kotlin. All new code must be written in Kotlin. +We try to follow the [Kotlin Style Guide](https://developer.android.com/kotlin/style-guide) and format the code according to the default [ktlint codestyle](https://github.com/pinterest/ktlint). +You can check the codestyle by running `./gradlew ktlintCheck lint`. This will fail if you have any errors, and produces a detailed report which also lists warnings. +We intentionally have very few hard linting errors, so that new contributors can focus on what they want to achieve instead of fighting the linter. + +### Text +All English text that will be visible to users must be put in `app/src/main/res/values/strings.xml` so it is translateable into other languages. +Try to keep texts friendly and concise. +If there is untranslatable text that you don't want to keep as a string constant in Kotlin code, you can use the string resource file `app/src/main/res/values/donottranslate.xml`. + +### Viewbinding +We use [Viewbinding](https://developer.android.com/topic/libraries/view-binding) to reference views. No contribution using another mechanism will be accepted. +There are useful extensions in `src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt` that make working with viewbinding easier. + +### Visuals +There are three themes in the app, so any visual changes should be checked with each of them to ensure they look appropriate no matter which theme is selected. Usually, you can use existing color attributes like `?attr/colorPrimary` and `?attr/textColorSecondary`. +All icons are from the Material iconset, find new icons [here](https://fonts.google.com/icons) (Google fonts) or [here](https://fonts.google.com/icons) (community contributions). + +### Accessibility +We try to make Tusky as accessible as possible for as many people as possible. Please make sure that all touch targets are at least 48dpx48dp in size, Text has sufficient contrast and images or icons have a image description. See [this guide](https://developer.android.com/guide/topics/ui/accessibility/apps) for more information. + +### Supported servers +Tusky is primarily a Mastodon client and aims to always support the newest Mastodon version. Other platforms implementing the Mastodon API, e.g. Akkoma, GoToSocial or Pixelfed should also work with Tusky, but no special effort is made to support their quirks or additional features. + +### Payment Policy +Our payment policy may be viewed [here](https://github.com/tuskyapp/Tusky/blob/develop/doc/PaymentPolicy.md). + +## Troubleshooting / FAQ + +- Tusky should be built with the newest version of Android Studio. +- Tusky comes with two sets of build variants, "blue" and "green", which can be installed simultaneously and are distinguished by the colors of their icons. Green is intended for local development and testing, whereas blue is for releases. + +## Resources +- [Mastodon API documentation](https://docs.joinmastodon.org/api/) diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..94a9ed0 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/> + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + <one line to give the program's name and a brief idea of what it does.> + Copyright (C) <year> <name of author> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + <program> Copyright (C) <year> <name of author> + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +<http://www.gnu.org/licenses/>. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +<http://www.gnu.org/philosophy/why-not-lgpl.html>. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1d43a59 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +[![Translate - with Weblate](https://img.shields.io/badge/translate%20with-Weblate-green.svg?style=flat)](https://weblate.tusky.app/) [![OpenCollective](https://opencollective.com/tusky/backers/badge.svg)](https://opencollective.com/tusky/) [![Build Status](https://app.bitrise.io/app/a3e773c3c57a894c/status.svg?token=qLu_Ti4Gp2LWcYT4eo2INQ&branch=develop)](https://app.bitrise.io/app/a3e773c3c57a894c) +# Crypty + +<img src="/fastlane/metadata/android/en-US/images/icon.png" width="120" height="120"/> + +Tusky is a beautiful Android client for [Mastodon](https://github.com/mastodon/mastodon). Mastodon is an ActivityPub federated social network. That means no single entity controls the whole network, rather, like e-mail, volunteers and organisations operate their own independent servers, users from which can all interact with each other seamlessly. +Crypty is a Tusky fork for supporting encrypted DMs. (WIP) + +[<img src="/assets/fdroid_badge.png" alt="Get it on F-Droid" height="80" />](https://f-droid.org/repository/browse/?fdid=com.keylesspalace.tusky) +[<img src="https://play.google.com/intl/en_us/badges/images/generic/en_badge_web_generic.png" alt="Get it on Google Play" height="80" />](https://play.google.com/store/apps/details?id=com.keylesspalace.tusky&utm_source=github&pcampaignid=MKT-Other-global-all-co-prtnr-py-PartBadge-Mar2515-1) + +## Features + +- Material Design +- Most Mastodon APIs implemented +- Multi-Account support +- Dark, light and black themes with the possibility to auto-switch based on the time of day +- Drafts - compose posts and save them for later +- Choose between different emoji styles +- Optimized for all screen sizes +- Completely open-source - no non-free dependencies like Google services + +### Testing + +The nightly build containing the newest development code is [available on Google Play](https://play.google.com/store/apps/details?id=com.keylesspalace.tusky.test). + +### Support + +Check out our [FAQs](https://github.com/tuskyapp/faq/blob/main/README.md), your question may already be answered. +If you have any bug reports, feature requests or questions please open an issue or send us a message at [Tusky@mastodon.social](https://mastodon.social/@Tusky)! + +### Contributing +We always welcome new contributors! Please read our [contribution guide](https://github.com/tuskyapp/Tusky/blob/develop/CONTRIBUTING.md) to get started. + +### Development chatroom +https://matrix.to/#/#Tusky:matrix.org diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..d53675d --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,194 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.google.ksp) + alias(libs.plugins.hilt.android) + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.parcelize) +} + +apply from: 'getGitSha.gradle' + +final def gitSha = ext.getGitSha() + +// The app name +final def APP_NAME = "Tusky" +// The application id. Must be unique, e.g. based on your domain +final def APP_ID = "com.keylesspalace.tusky" +// url of a custom app logo. Recommended size at least 600x600. Keep empty to use the Tusky elephant friend. +final def CUSTOM_LOGO_URL = "" +// e.g. mastodon.social. Keep empty to not suggest any instance on the signup screen +final def CUSTOM_INSTANCE = "" +// link to your support account. Will be linked on the about page when not empty. +final def SUPPORT_ACCOUNT_URL = "https://mastodon.social/@Tusky" + +android { + compileSdk 34 + namespace "com.keylesspalace.tusky" + defaultConfig { + applicationId APP_ID + namespace "com.keylesspalace.tusky" + minSdk 24 + targetSdk 34 + versionCode 121 + versionName "25.2" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables.useSupportLibrary = true + + resValue "string", "app_name", APP_NAME + + buildConfigField("String", "CUSTOM_LOGO_URL", "\"$CUSTOM_LOGO_URL\"") + buildConfigField("String", "CUSTOM_INSTANCE", "\"$CUSTOM_INSTANCE\"") + buildConfigField("String", "SUPPORT_ACCOUNT_URL", "\"$SUPPORT_ACCOUNT_URL\"") + } + buildTypes { + debug { + isDefault true + } + release { + minifyEnabled true + shrinkResources true + proguardFiles 'proguard-rules.pro' + + kotlinOptions { + freeCompilerArgs = [ + "-Xno-param-assertions", + "-Xno-call-assertions", + "-Xno-receiver-assertions" + ] + } + } + } + + flavorDimensions += "color" + productFlavors { + blue {} + green { + resValue "string", "app_name", APP_NAME + " Test" + applicationIdSuffix ".test" + versionNameSuffix "-" + gitSha + isDefault true + } + } + + lint { + lintConfig file("lint.xml") + // Regenerate by deleting the file and running `./gradlew app:lintGreenDebug` + baseline = file("lint-baseline.xml") + } + + buildFeatures { + buildConfig true + resValues true + viewBinding true + } + + testOptions { + unitTests { + returnDefaultValues = true + includeAndroidResources = true + } + unitTests.all { + systemProperty 'robolectric.logging.enabled', 'true' + systemProperty 'robolectric.lazyload', 'ON' + } + } + sourceSets { + androidTest.assets.srcDirs += files("$projectDir/schemas".toString()) + } + + // Exclude unneeded files added by libraries + packagingOptions.resources.excludes += [ + 'LICENSE_OFL', + 'LICENSE_UNICODE', + ] + + bundle { + language { + // bundle all languages in every apk so the dynamic language switching works + enableSplit = false + } + } + dependenciesInfo { + includeInApk false + includeInBundle false + } + applicationVariants.configureEach { variant -> + variant.outputs.configureEach { + outputFileName = "Tusky_${variant.versionName}_${variant.versionCode}_${gitSha}_" + + "${variant.flavorName}_${buildType.name}.apk" + } + } +} + +ksp { + arg("room.schemaLocation", "$projectDir/schemas") + arg("room.generateKotlin", "true") + arg("room.incremental", "true") +} + +configurations { + // JNI-only libraries don't play nicely with Robolectric + // see https://github.com/tuskyapp/Tusky/pull/3367 + testImplementation.exclude group: "org.conscrypt", module: "conscrypt-android" + testRuntime.exclude group: "org.conscrypt", module: "conscrypt-android" +} + +// library versions are in PROJECT_ROOT/gradle/libs.versions.toml +dependencies { + implementation libs.kotlinx.coroutines.android + + implementation libs.bundles.androidx + implementation libs.bundles.room + ksp libs.androidx.room.compiler + + implementation libs.android.material + + implementation libs.bundles.moshi + ksp libs.moshi.kotlin.codegen + + implementation libs.bundles.retrofit + implementation libs.networkresult.calladapter + + implementation libs.bundles.okhttp + implementation libs.okio + + implementation libs.conscrypt.android + + implementation libs.bundles.glide + ksp libs.glide.compiler + + implementation libs.hilt.android + ksp libs.hilt.compiler + implementation libs.androidx.hilt.work + ksp libs.androidx.hilt.compiler + + implementation libs.sparkbutton + + implementation libs.touchimageview + + implementation libs.bundles.material.drawer + implementation libs.material.typeface + + implementation libs.image.cropper + + implementation libs.bundles.filemojicompat + + implementation libs.bouncycastle + implementation libs.unified.push + + implementation libs.bundles.xmldiff + + testImplementation libs.androidx.test.junit + testImplementation libs.robolectric + testImplementation libs.bundles.mockito + testImplementation libs.mockwebserver + testImplementation libs.androidx.core.testing + testImplementation libs.kotlinx.coroutines.test + testImplementation libs.androidx.work.testing + testImplementation libs.truth + testImplementation libs.turbine + + androidTestImplementation libs.espresso.core + androidTestImplementation libs.androidx.room.testing + androidTestImplementation libs.androidx.test.junit +} diff --git a/app/getGitSha.gradle b/app/getGitSha.gradle new file mode 100644 index 0000000..53315b2 --- /dev/null +++ b/app/getGitSha.gradle @@ -0,0 +1,27 @@ +import org.gradle.api.provider.ValueSourceParameters +import javax.inject.Inject + +// Must wrap this in a ValueSource in order to get well-defined fail behavior without confusing Gradle on repeat builds. +abstract class GitShaValueSource implements ValueSource<String, ValueSourceParameters.None> { + @Inject abstract ExecOperations getExecOperations() + + @Override String obtain() { + try { + def output = new ByteArrayOutputStream() + + execOperations.exec { + it.commandLine 'git', 'rev-parse', '--short=8', 'HEAD' + it.standardOutput = output + } + return output.toString().trim() + } catch (GradleException ignore) { + // Git executable unavailable, or we are not building in a git repo. Fall through: + } + return "unknown" + } +} + +// Export closure +ext.getGitSha = { + providers.of(GitShaValueSource) {}.get() +} diff --git a/app/lint-baseline.xml b/app/lint-baseline.xml new file mode 100644 index 0000000..aa4ba09 --- /dev/null +++ b/app/lint-baseline.xml @@ -0,0 +1,886 @@ +<?xml version="1.0" encoding="UTF-8"?> +<issues format="6" by="lint 8.3.2" type="baseline" client="gradle" dependencies="false" name="AGP (8.3.2)" variant="all" version="8.3.2"> + + <issue + id="GestureBackNavigation" + message="If intercepting back events, this should be handled through the registration of callbacks on the window level; Please see https://developer.android.com/about/versions/13/features/predictive-back-gesture" + errorLine1=" if (keyCode == KeyEvent.KEYCODE_BACK) {" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt" + line="1314" + column="28"/> + </issue> + + <issue + id="DefaultLocale" + message="Implicitly using the default locale is a common source of bugs: Use `toUpperCase(Locale)` instead. For strings meant to be internal use `Locale.ROOT`, otherwise `Locale.getDefault()`." + errorLine1=" sb.append(language.toUpperCase());" + errorLine2=" ~~~~~~~~~~~"> + <location + file="src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java" + line="104" + column="32"/> + </issue> + + <issue + id="InvalidPackage" + message="Invalid package reference in org.bouncycastle:bcprov-jdk15on; not included in Android: `javax.naming.directory`. Referenced from `org.bouncycastle.jce.provider.CrlCache`."> + <location + file="$GRADLE_USER_HOME/caches/modules-2/files-2.1/org.bouncycastle/bcprov-jdk15on/1.70/4636a0d01f74acaf28082fb62b317f1080118371/bcprov-jdk15on-1.70.jar"/> + </issue> + + <issue + id="InvalidPackage" + message="Invalid package reference in org.bouncycastle:bcprov-jdk15on; not included in Android: `javax.naming`. Referenced from `org.bouncycastle.jce.provider.X509LDAPCertStoreSpi`."> + <location + file="$GRADLE_USER_HOME/caches/modules-2/files-2.1/org.bouncycastle/bcprov-jdk15on/1.70/4636a0d01f74acaf28082fb62b317f1080118371/bcprov-jdk15on-1.70.jar"/> + </issue> + + <issue + id="InvalidPackage" + message="Invalid package reference in org.pageseeder.diffx:pso-diffx; not included in Android: `javax.xml.stream.events`. Referenced from `org.pageseeder.diffx.load.XMLEventLoader`."> + <location + file="$GRADLE_USER_HOME/caches/modules-2/files-2.1/org.pageseeder.diffx/pso-diffx/1.1.1/b655ebc87588a857a4f3d88cf98bcefa87a6105b/pso-diffx-1.1.1.jar"/> + </issue> + + <issue + id="InvalidPackage" + message="Invalid package reference in org.pageseeder.diffx:pso-diffx; not included in Android: `javax.xml.stream`. Referenced from `org.pageseeder.diffx.format.StrictXMLDiffOutput`."> + <location + file="$GRADLE_USER_HOME/caches/modules-2/files-2.1/org.pageseeder.diffx/pso-diffx/1.1.1/b655ebc87588a857a4f3d88cf98bcefa87a6105b/pso-diffx-1.1.1.jar"/> + </issue> + + <issue + id="PrivateResource" + message="Overriding `@layout/exo_player_control_view` which is marked as private in androidx.media3:media3-ui:1.3.1. If deliberate, use tools:override="true", otherwise pick a different name."> + <location + file="src/main/res/layout/exo_player_control_view.xml"/> + </issue> + + <issue + id="PrivateResource" + message="The resource `@color/exo_bottom_bar_background` is marked as private in androidx.media3:media3-ui:1.3.1" + errorLine1=" android:background="@color/exo_bottom_bar_background"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="39" + column="29"/> + </issue> + + <issue + id="PrivateResource" + message="The resource `@dimen/exo_styled_controls_padding` is marked as private in androidx.media3:media3-ui:1.3.1" + errorLine1=" android:padding="@dimen/exo_styled_controls_padding"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="41" + column="26"/> + </issue> + + <issue + id="PrivateResource" + message="The resource `@layout/exo_player_control_rewind_button` is marked as private in androidx.media3:media3-ui:1.3.1" + errorLine1=" <include layout="@layout/exo_player_control_rewind_button" />" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="48" + column="26"/> + </issue> + + <issue + id="PrivateResource" + message="The resource `@layout/exo_player_control_ffwd_button` is marked as private in androidx.media3:media3-ui:1.3.1" + errorLine1=" <include layout="@layout/exo_player_control_ffwd_button" />" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="54" + column="26"/> + </issue> + + <issue + id="PrivateResource" + message="The resource `@dimen/exo_styled_bottom_bar_height` is marked as private in androidx.media3:media3-ui:1.3.1" + errorLine1=" android:layout_height="@dimen/exo_styled_bottom_bar_height"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="63" + column="32"/> + </issue> + + <issue + id="PrivateResource" + message="The resource `@dimen/exo_styled_bottom_bar_margin_top` is marked as private in androidx.media3:media3-ui:1.3.1" + errorLine1=" android:layout_marginTop="@dimen/exo_styled_bottom_bar_margin_top"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="64" + column="35"/> + </issue> + + <issue + id="PrivateResource" + message="The resource `@color/exo_bottom_bar_background` is marked as private in androidx.media3:media3-ui:1.3.1" + errorLine1=" android:background="@color/exo_bottom_bar_background"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="66" + column="29"/> + </issue> + + <issue + id="PrivateResource" + message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.3.1" + errorLine1=" android:paddingStart="@dimen/exo_styled_bottom_bar_time_padding"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="72" + column="35"/> + </issue> + + <issue + id="PrivateResource" + message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.3.1" + errorLine1=" android:paddingEnd="@dimen/exo_styled_bottom_bar_time_padding"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="73" + column="33"/> + </issue> + + <issue + id="PrivateResource" + message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.3.1" + errorLine1=" android:paddingLeft="@dimen/exo_styled_bottom_bar_time_padding"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="74" + column="34"/> + </issue> + + <issue + id="PrivateResource" + message="The resource `@dimen/exo_styled_bottom_bar_time_padding` is marked as private in androidx.media3:media3-ui:1.3.1" + errorLine1=" android:paddingRight="@dimen/exo_styled_bottom_bar_time_padding"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="75" + column="35"/> + </issue> + + <issue + id="PrivateResource" + message="The resource `@dimen/exo_styled_progress_layout_height` is marked as private in androidx.media3:media3-ui:1.3.1" + errorLine1=" android:layout_height="@dimen/exo_styled_progress_layout_height"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="141" + column="32"/> + </issue> + + <issue + id="PrivateResource" + message="The resource `@dimen/exo_styled_progress_margin_bottom` is marked as private in androidx.media3:media3-ui:1.3.1" + errorLine1=" android:layout_marginBottom="@dimen/exo_styled_progress_margin_bottom"/>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="143" + column="38"/> + </issue> + + <issue + id="PrivateResource" + message="The resource `@dimen/exo_styled_minimal_controls_margin_bottom` is marked as private in androidx.media3:media3-ui:1.3.1" + errorLine1=" android:layout_marginBottom="@dimen/exo_styled_minimal_controls_margin_bottom"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="149" + column="38"/> + </issue> + + <issue + id="PluralsCandidate" + message="Formatting %d followed by words ("posts"): This should probably be a plural rather than a string" + errorLine1=" <string name="notification_summary_report_format">%s · %d posts attached</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/values/strings.xml" + line="109" + column="5"/> + </issue> + + <issue + id="PluralsCandidate" + message="Formatting %d followed by words ("and"): This should probably be a plural rather than a string" + errorLine1=" <string name="pref_title_http_proxy_port_message">Port should be between %d and %d</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/values/strings.xml" + line="331" + column="5"/> + </issue> + + <issue + id="PluralsCandidate" + message="Formatting %d followed by words ("others"): This should probably be a plural rather than a string" + errorLine1=" <string name="notification_summary_large">%1$s, %2$s, %3$s and %4$d others</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/values/strings.xml" + line="389" + column="5"/> + </issue> + + <issue + id="PluralsCandidate" + message="Formatting %d followed by words ("more"): This should probably be a plural rather than a string" + errorLine1=" <string name="conversation_more_recipients">%1$s, %2$s and %3$d more</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/values/strings.xml" + line="570" + column="5"/> + </issue> + + <issue + id="PluralsCandidate" + message="Formatting %d followed by words ("people"): This should probably be a plural rather than a string" + errorLine1=" <string name="accessibility_talking_about_tag">%1$d people are talking about hashtag %2$s</string>" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/values/strings.xml" + line="790" + column="5"/> + </issue> + + <issue + id="UnusedTranslation" + message="The language `ber (Berber languages)` is present in this project, but not declared in the `localeConfig` resource" + errorLine1=" android:localeConfig="@xml/locales_config"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/AndroidManifest.xml" + line="21" + column="31"/> + </issue> + + <issue + id="UnusedTranslation" + message="The language `el (Greek)` is present in this project, but not declared in the `localeConfig` resource" + errorLine1=" android:localeConfig="@xml/locales_config"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/AndroidManifest.xml" + line="21" + column="31"/> + </issue> + + <issue + id="UnusedTranslation" + message="The language `fi (Finnish)` is present in this project, but not declared in the `localeConfig` resource" + errorLine1=" android:localeConfig="@xml/locales_config"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/AndroidManifest.xml" + line="21" + column="31"/> + </issue> + + <issue + id="UnusedTranslation" + message="The language `fy (Western Frisian)` is present in this project, but not declared in the `localeConfig` resource" + errorLine1=" android:localeConfig="@xml/locales_config"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/AndroidManifest.xml" + line="21" + column="31"/> + </issue> + + <issue + id="UnusedTranslation" + message="The language `in (Indonesian)` is present in this project, but not declared in the `localeConfig` resource" + errorLine1=" android:localeConfig="@xml/locales_config"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/AndroidManifest.xml" + line="21" + column="31"/> + </issue> + + <issue + id="UnusedTranslation" + message="The language `lv (Latvian)` is present in this project, but not declared in the `localeConfig` resource" + errorLine1=" android:localeConfig="@xml/locales_config"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/AndroidManifest.xml" + line="21" + column="31"/> + </issue> + + <issue + id="UnusedTranslation" + message="The language `ml (Malayalam)` is present in this project, but not declared in the `localeConfig` resource" + errorLine1=" android:localeConfig="@xml/locales_config"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/AndroidManifest.xml" + line="21" + column="31"/> + </issue> + + <issue + id="UnusedTranslation" + message="The language `si (Sinhala)` is present in this project, but not declared in the `localeConfig` resource" + errorLine1=" android:localeConfig="@xml/locales_config"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/AndroidManifest.xml" + line="21" + column="31"/> + </issue> + + <issue + id="UnusedTranslation" + message="The language `sk (Slovak)` is present in this project, but not declared in the `localeConfig` resource" + errorLine1=" android:localeConfig="@xml/locales_config"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/AndroidManifest.xml" + line="21" + column="31"/> + </issue> + + <issue + id="DataExtractionRules" + message="The attribute `android:allowBackup` is deprecated from Android 12 and higher and may be removed in future versions. Consider adding the attribute `android:dataExtractionRules` specifying an `@xml` resource which configures cloud backups and device transfers on Android 12 and higher." + errorLine1=" android:allowBackup="false"" + errorLine2=" ~~~~~"> + <location + file="src/main/AndroidManifest.xml" + line="15" + column="30"/> + </issue> + + <issue + id="NotifyDataSetChanged" + message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort." + errorLine1=" accountFieldAdapter.notifyDataSetChanged()" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt" + line="532" + column="9"/> + </issue> + + <issue + id="NotifyDataSetChanged" + message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort." + errorLine1=" notifyDataSetChanged()" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt" + line="78" + column="9"/> + </issue> + + <issue + id="NotifyDataSetChanged" + message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort." + errorLine1=" notifyDataSetChanged()" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt" + line="45" + column="9"/> + </issue> + + <issue + id="NotifyDataSetChanged" + message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort." + errorLine1=" notifyDataSetChanged()" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt" + line="51" + column="9"/> + </issue> + + <issue + id="NotifyDataSetChanged" + message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort." + errorLine1=" notifyDataSetChanged()" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt" + line="157" + column="9"/> + </issue> + + <issue + id="NotifyDataSetChanged" + message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort." + errorLine1=" notifyDataSetChanged()" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/MutesAdapter.kt" + line="118" + column="9"/> + </issue> + + <issue + id="NotifyDataSetChanged" + message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort." + errorLine1=" notifyDataSetChanged()" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt" + line="62" + column="9"/> + </issue> + + <issue + id="NotifyDataSetChanged" + message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort." + errorLine1=" notifyDataSetChanged()" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt" + line="62" + column="9"/> + </issue> + + <issue + id="NotifyDataSetChanged" + message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort." + errorLine1=" notifyDataSetChanged()" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt" + line="35" + column="9"/> + </issue> + + <issue + id="NotifyDataSetChanged" + message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort." + errorLine1=" notifyDataSetChanged()" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt" + line="54" + column="9"/> + </issue> + + <issue + id="NotifyDataSetChanged" + message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort." + errorLine1=" notifyDataSetChanged()" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt" + line="161" + column="13"/> + </issue> + + <issue + id="SmallSp" + message="Avoid using sizes smaller than `11sp`: `8sp`" + errorLine1=" android:textSize="8sp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout-land/item_trending_cell.xml" + line="42" + column="9"/> + </issue> + + <issue + id="SmallSp" + message="Avoid using sizes smaller than `11sp`: `8sp`" + errorLine1=" android:textSize="8sp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/item_trending_cell.xml" + line="42" + column="9"/> + </issue> + + <issue + id="SmallSp" + message="Avoid using sizes smaller than `11sp`: `8sp`" + errorLine1=" android:textSize="8sp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout-land/item_trending_cell.xml" + line="59" + column="9"/> + </issue> + + <issue + id="SmallSp" + message="Avoid using sizes smaller than `11sp`: `8sp`" + errorLine1=" android:textSize="8sp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/item_trending_cell.xml" + line="59" + column="9"/> + </issue> + + <issue + id="ReportShortcutUsage" + message="Calling this method indicates use of dynamic shortcuts, but there are no calls to methods that track shortcut usage, such as `pushDynamicShortcut` or `reportShortcutUsed`. Calling these methods is recommended, as they track shortcut usage and allow launchers to adjust which shortcuts appear based on activation history. Please see https://developer.android.com/develop/ui/views/launch/shortcuts/managing-shortcuts#track-usage" + errorLine1=" ShortcutManagerCompat.addDynamicShortcuts(context, listOf(shortcutInfo))" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt" + line="96" + column="13"/> + </issue> + + <issue + id="ClickableViewAccessibility" + message="Custom view ``ImageView`` has `setOnTouchListener` called on it but does not override `performClick`" + errorLine1=" binding.imageView.setOnTouchListener { _, event ->" + errorLine2=" ^"> + <location + file="src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt" + line="95" + column="13"/> + </issue> + + <issue + id="ClickableViewAccessibility" + message="`onTouch` lambda should call `View#performClick` when a click is detected" + errorLine1=" binding.imageView.setOnTouchListener { _, event ->" + errorLine2=" ^"> + <location + file="src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt" + line="95" + column="50"/> + </issue> + + <issue + id="ContentDescription" + message="Missing `contentDescription` attribute on image" + errorLine1=" <ImageButton android:id="@id/exo_prev"" + errorLine2=" ~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="45" + column="10"/> + </issue> + + <issue + id="ContentDescription" + message="Missing `contentDescription` attribute on image" + errorLine1=" <ImageButton android:id="@id/exo_play_pause"" + errorLine2=" ~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="51" + column="10"/> + </issue> + + <issue + id="ContentDescription" + message="Missing `contentDescription` attribute on image" + errorLine1=" <ImageButton android:id="@id/exo_next"" + errorLine2=" ~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="56" + column="10"/> + </issue> + + <issue + id="ContentDescription" + message="Missing `contentDescription` attribute on image" + errorLine1=" <ImageButton android:id="@id/exo_vr"" + errorLine2=" ~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="96" + column="14"/> + </issue> + + <issue + id="ContentDescription" + message="Missing `contentDescription` attribute on image" + errorLine1=" <ImageButton android:id="@id/exo_shuffle"" + errorLine2=" ~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="99" + column="14"/> + </issue> + + <issue + id="ContentDescription" + message="Missing `contentDescription` attribute on image" + errorLine1=" <ImageButton android:id="@id/exo_repeat_toggle"" + errorLine2=" ~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="102" + column="14"/> + </issue> + + <issue + id="ContentDescription" + message="Missing `contentDescription` attribute on image" + errorLine1=" <ImageButton android:id="@id/exo_subtitle"" + errorLine2=" ~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="105" + column="14"/> + </issue> + + <issue + id="ContentDescription" + message="Missing `contentDescription` attribute on image" + errorLine1=" <ImageButton android:id="@id/exo_settings"" + errorLine2=" ~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="108" + column="14"/> + </issue> + + <issue + id="ContentDescription" + message="Missing `contentDescription` attribute on image" + errorLine1=" <ImageButton android:id="@id/exo_fullscreen"" + errorLine2=" ~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="111" + column="14"/> + </issue> + + <issue + id="ContentDescription" + message="Missing `contentDescription` attribute on image" + errorLine1=" <ImageButton android:id="@id/exo_overflow_show"" + errorLine2=" ~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="114" + column="14"/> + </issue> + + <issue + id="ContentDescription" + message="Missing `contentDescription` attribute on image" + errorLine1=" <ImageButton android:id="@id/exo_overflow_hide"" + errorLine2=" ~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="130" + column="18"/> + </issue> + + <issue + id="ContentDescription" + message="Missing `contentDescription` attribute on image" + errorLine1=" <ImageButton android:id="@id/exo_minimal_fullscreen"" + errorLine2=" ~~~~~~~~~~~"> + <location + file="src/main/res/layout/exo_player_control_view.xml" + line="154" + column="10"/> + </issue> + + <issue + id="RtlSymmetry" + message="When you define `paddingStart` you should probably also define `paddingEnd` for right-to-left symmetry" + errorLine1=" android:paddingStart="8dp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/activity_edit_profile.xml" + line="118" + column="21"/> + </issue> + + <issue + id="RtlSymmetry" + message="When you define `paddingStart` you should probably also define `paddingEnd` for right-to-left symmetry" + errorLine1=" android:paddingStart="40dp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/activity_edit_profile.xml" + line="129" + column="21"/> + </issue> + + <issue + id="RtlSymmetry" + message="When you define `paddingEnd` you should probably also define `paddingStart` for right-to-left symmetry" + errorLine1=" android:paddingEnd="@dimen/status_display_name_padding_end"" + errorLine2=" ~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/item_conversation.xml" + line="90" + column="9"/> + </issue> + + <issue + id="RtlSymmetry" + message="When you define `paddingStart` you should probably also define `paddingEnd` for right-to-left symmetry" + errorLine1=" android:paddingStart="28dp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/item_follow.xml" + line="21" + column="9"/> + </issue> + + <issue + id="RtlSymmetry" + message="When you define `paddingStart` you should probably also define `paddingEnd` for right-to-left symmetry" + errorLine1=" android:paddingStart="28dp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/item_report_notification.xml" + line="23" + column="9"/> + </issue> + + <issue + id="RtlSymmetry" + message="When you define `paddingEnd` you should probably also define `paddingStart` for right-to-left symmetry" + errorLine1=" android:paddingEnd="@dimen/status_display_name_padding_end"" + errorLine2=" ~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/item_status.xml" + line="68" + column="9"/> + </issue> + + <issue + id="RtlSymmetry" + message="When you define `paddingStart` you should probably also define `paddingEnd` for right-to-left symmetry" + errorLine1=" android:paddingStart="28dp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/item_status_notification.xml" + line="22" + column="9"/> + </issue> + + <issue + id="RtlSymmetry" + message="When you define `paddingStart` you should probably also define `paddingEnd` for right-to-left symmetry" + errorLine1=" android:paddingStart="6dp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout-land/item_trending_cell.xml" + line="38" + column="9"/> + </issue> + + <issue + id="RtlSymmetry" + message="When you define `paddingStart` you should probably also define `paddingEnd` for right-to-left symmetry" + errorLine1=" android:paddingStart="6dp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/item_trending_cell.xml" + line="38" + column="9"/> + </issue> + + <issue + id="RtlSymmetry" + message="When you define `paddingStart` you should probably also define `paddingEnd` for right-to-left symmetry" + errorLine1=" android:paddingStart="6dp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout-land/item_trending_cell.xml" + line="55" + column="9"/> + </issue> + + <issue + id="RtlSymmetry" + message="When you define `paddingStart` you should probably also define `paddingEnd` for right-to-left symmetry" + errorLine1=" android:paddingStart="6dp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/item_trending_cell.xml" + line="55" + column="9"/> + </issue> + + <issue + id="RtlSymmetry" + message="When you define `paddingStart` you should probably also define `paddingEnd` for right-to-left symmetry" + errorLine1=" android:paddingStart="4dp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/view_compose_schedule.xml" + line="21" + column="9"/> + </issue> + + <issue + id="RtlSymmetry" + message="When you define `paddingStart` you should probably also define `paddingEnd` for right-to-left symmetry" + errorLine1=" android:paddingStart="4dp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/view_compose_schedule.xml" + line="38" + column="9"/> + </issue> + + <issue + id="RtlHardcoded" + message="Consider replacing `android:layout_marginLeft` with `android:layout_marginStart="8dp"` to better support right-to-left layouts" + errorLine1=" android:layout_marginLeft="8dp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/item_list.xml" + line="37" + column="9"/> + </issue> + + <issue + id="RtlHardcoded" + message="Consider replacing `android:layout_marginLeft` with `android:layout_marginStart="8dp"` to better support right-to-left layouts" + errorLine1=" android:layout_marginLeft="8dp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/item_list.xml" + line="48" + column="9"/> + </issue> + + <issue + id="RtlHardcoded" + message="Consider replacing `android:layout_marginLeft` with `android:layout_marginStart="8dp"` to better support right-to-left layouts" + errorLine1=" android:layout_marginLeft="8dp"" + errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~"> + <location + file="src/main/res/layout/item_list.xml" + line="59" + column="9"/> + </issue> + +</issues> diff --git a/app/lint.xml b/app/lint.xml new file mode 100644 index 0000000..88ad21f --- /dev/null +++ b/app/lint.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + ~ Copyright 2023 Tusky Contributors + ~ + ~ This file is a part of Tusky. + ~ + ~ This program is free software; you can redistribute it and/or modify it under the terms of the + ~ GNU General Public License as published by the Free Software Foundation; either version 3 of the + ~ License, or (at your option) any later version. + ~ + ~ Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + ~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + ~ Public License for more details. + ~ + ~ You should have received a copy of the GNU General Public License along with Tusky; if not, + ~ see <http://www.gnu.org/licenses>. + --> + +<lint> + <!-- Missing translations are OK --> + <issue id="MissingTranslation" severity="ignore" /> + + <!-- Duplicate strings are OK. This can happen when e.g., "favourite" appears as both + a noun and a verb --> + <issue id="DuplicateStrings" severity="ignore" /> + + <!-- Resource IDs used in viewbinding are incorrectly reported as unused, + https://issuetracker.google.com/issues/204797401. + + Disable these for the time being. --> + <issue id="UnusedIds" severity="ignore" /> + <issue id="UnusedResources" severity="ignore" /> + + <!-- Logs are stripped in release builds. --> + <issue id="LogConditional" severity="ignore" /> + + <!-- Newer dependencies are handled by Renovate, and don't need a warning --> + <issue id="GradleDependency" severity="ignore" /> + <issue id="NewerVersionAvailable" severity="ignore" /> + + <!-- Typographical punctuation is not something we care about at the moment --> + <issue id="TypographyQuotes" severity="ignore" /> + <issue id="TypographyDashes" severity="ignore" /> + <issue id="TypographyEllipsis" severity="ignore" /> + + <!-- Translations come from external parties --> + <issue id="MissingQuantity" severity="ignore" /> + <issue id="ImpliedQuantity" severity="ignore" /> + <!-- Most alleged typos are in translations --> + <issue id="Typos" severity="ignore" /> + + <!-- Basically all of our vectors are external --> + <issue id="VectorPath" severity="ignore" /> + <issue id="Overdraw" severity="ignore" /> + + <!-- Irrelevant api version warnings --> + <issue id="OldTargetApi" severity="ignore" /> + <issue id="UnusedAttribute" severity="ignore" /> + + <!-- We do not *want* all the text in the app to be selectable --> + <issue id="SelectableText" severity="ignore" /> + + <!-- This is heavily used by the viewbinding helper --> + <issue id="SyntheticAccessor" severity="ignore" /> + + <!-- Things we would actually question in a code review --> + <issue id="MissingPermission" severity="error" /> + <issue id="InvalidPackage" severity="error" /> + <issue id="UseCompatLoadingForDrawables" severity="error" /> + <issue id="UseCompatTextViewDrawableXml" severity="error" /> + <issue id="Recycle" severity="error" /> + <issue id="KeyboardInaccessibleWidget" severity="error" /> + + <!-- Mark all other lint issues as warnings --> + <issue id="all" severity="warning" /> +</lint> diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..0bcca6c --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,78 @@ +# GENERAL OPTIONS + +-allowaccessmodification + +# Preserve some attributes that may be required for reflection. +-keepattributes RuntimeVisible*Annotations, AnnotationDefault + +# For native methods, see http://proguard.sourceforge.net/manual/examples.html#native +-keepclasseswithmembernames class * { + native <methods>; +} + +# For enumeration classes, see http://proguard.sourceforge.net/manual/examples.html#enumerations +-keepclassmembers,allowoptimization enum * { + public static **[] values(); + public static ** valueOf(java.lang.String); +} + +-keepclassmembers class * implements android.os.Parcelable { + public static final ** CREATOR; +} + +# Preserve annotated Javascript interface methods. +-keepclassmembers class * { + @android.webkit.JavascriptInterface <methods>; +} + +# The support libraries contains references to newer platform versions. +# Don't warn about those in case this app is linking against an older +# platform version. We know about them, and they are safe. +-dontnote androidx.** +-dontwarn androidx.** + +# This class is deprecated, but remains for backward compatibility. +-dontwarn android.util.FloatMath + +# These classes are duplicated between android.jar and core-lambda-stubs.jar. +-dontnote java.lang.invoke.** + +# TUSKY SPECIFIC OPTIONS + +# preserve line numbers for crash reporting +-keepattributes SourceFile,LineNumberTable +-renamesourcefileattribute SourceFile + +# Bouncy Castle -- Keep EC +-keep class org.bouncycastle.jcajce.provider.asymmetric.EC$* { *; } +-keep class org.bouncycastle.jcajce.provider.asymmetric.ec.KeyPairGeneratorSpi$EC + +# remove all logging from production apk +-assumenosideeffects class android.util.Log { + public static *** getStackTraceString(...); + public static *** d(...); + public static *** w(...); + public static *** v(...); + public static *** i(...); +} +-assumenosideeffects class java.lang.String { + public static java.lang.String format(...); +} + +# remove some kotlin overhead +-assumenosideeffects class kotlin.jvm.internal.Intrinsics { + static void checkNotNull(java.lang.Object); + static void checkNotNull(java.lang.Object, java.lang.String); + static void checkParameterIsNotNull(java.lang.Object, java.lang.String); + static void checkParameterIsNotNull(java.lang.Object, java.lang.String); + static void checkNotNullParameter(java.lang.Object, java.lang.String); + static void checkExpressionValueIsNotNull(java.lang.Object, java.lang.String); + static void checkNotNullExpressionValue(java.lang.Object, java.lang.String); + static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String); + static void checkReturnedValueIsNotNull(java.lang.Object, java.lang.String, java.lang.String); + static void throwUninitializedPropertyAccessException(java.lang.String); +} + +# Preference fragments can be referenced by name, ensure they remain +# https://github.com/tuskyapp/Tusky/issues/3161 +-keep class * extends androidx.preference.PreferenceFragmentCompat diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/10.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/10.json new file mode 100644 index 0000000..f1f83ff --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/10.json @@ -0,0 +1,275 @@ +{ + "formatVersion": 1, + "database": { + "version": 10, + "identityHash": "69e310ef98c0f305934d25e763ee0140", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"69e310ef98c0f305934d25e763ee0140\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/11.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/11.json new file mode 100644 index 0000000..fe3fb45 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/11.json @@ -0,0 +1,515 @@ +{ + "formatVersion": 1, + "database": { + "version": 11, + "identityHash": "f5e93302cf53d4250e455b701bea102f", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `instance` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `instance` TEXT NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"f5e93302cf53d4250e455b701bea102f\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/12.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/12.json new file mode 100644 index 0000000..c217590 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/12.json @@ -0,0 +1,668 @@ +{ + "formatVersion": 1, + "database": { + "version": 12, + "identityHash": "d4d3d4c683ab7f681459b9edab92301c", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `instance` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `instance` TEXT NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"d4d3d4c683ab7f681459b9edab92301c\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/13.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/13.json new file mode 100644 index 0000000..ba7e57b --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/13.json @@ -0,0 +1,656 @@ +{ + "formatVersion": 1, + "database": { + "version": 13, + "identityHash": "9a63a3ab2c05004022c350aab0e472c0", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"9a63a3ab2c05004022c350aab0e472c0\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/14.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/14.json new file mode 100644 index 0000000..85c8028 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/14.json @@ -0,0 +1,662 @@ +{ + "formatVersion": 1, + "database": { + "version": 14, + "identityHash": "b9ca62605345d229ced2bb0c1f2db79b", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"b9ca62605345d229ced2bb0c1f2db79b\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/15.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/15.json new file mode 100644 index 0000000..eb57584 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/15.json @@ -0,0 +1,674 @@ +{ + "formatVersion": 1, + "database": { + "version": 15, + "identityHash": "6a01315ce9f7d402cb61e611140e3c0a", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"6a01315ce9f7d402cb61e611140e3c0a\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/16.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/16.json new file mode 100644 index 0000000..a4c4482 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/16.json @@ -0,0 +1,680 @@ +{ + "formatVersion": 1, + "database": { + "version": 16, + "identityHash": "821df8c72aa78a288b4ae9fe2df21dda", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"821df8c72aa78a288b4ae9fe2df21dda\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/17.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/17.json new file mode 100644 index 0000000..15a7b5e --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/17.json @@ -0,0 +1,686 @@ +{ + "formatVersion": 1, + "database": { + "version": 17, + "identityHash": "4e6bfccf6ec0812dc0bc58d5bc8cf556", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, \"4e6bfccf6ec0812dc0bc58d5bc8cf556\")" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/18.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/18.json new file mode 100644 index 0000000..0319e67 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/18.json @@ -0,0 +1,693 @@ +{ + "formatVersion": 1, + "database": { + "version": 18, + "identityHash": "33d7d9b8ba14c87b96ce795c337bfc57", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '33d7d9b8ba14c87b96ce795c337bfc57')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/19.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/19.json new file mode 100644 index 0000000..0d62b12 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/19.json @@ -0,0 +1,711 @@ +{ + "formatVersion": 1, + "database": { + "version": 19, + "identityHash": "84ebd39cba4d6749251d330851b70e36", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '84ebd39cba4d6749251d330851b70e36')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/20.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/20.json new file mode 100644 index 0000000..ca6e30a --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/20.json @@ -0,0 +1,723 @@ +{ + "formatVersion": 1, + "database": { + "version": 20, + "identityHash": "611700a54bdc155d6bc9d87b8b2af2aa", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '611700a54bdc155d6bc9d87b8b2af2aa')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/21.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/21.json new file mode 100644 index 0000000..7845dad --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/21.json @@ -0,0 +1,729 @@ +{ + "formatVersion": 1, + "database": { + "version": 21, + "identityHash": "7570c84ffeb4f90521f91dc7ef3e7da1", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7570c84ffeb4f90521f91dc7ef3e7da1')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/22.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/22.json new file mode 100644 index 0000000..f5508a8 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/22.json @@ -0,0 +1,735 @@ +{ + "formatVersion": 1, + "database": { + "version": 22, + "identityHash": "eaa3c4d012fe743948343983fe1ae493", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'eaa3c4d012fe743948343983fe1ae493')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/23.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/23.json new file mode 100644 index 0000000..d7f2b29 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/23.json @@ -0,0 +1,741 @@ +{ + "formatVersion": 1, + "database": { + "version": 23, + "identityHash": "03a7436643ef356198742c5f8e054f5f", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '03a7436643ef356198742c5f8e054f5f')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json new file mode 100644 index 0000000..c47a305 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/24.json @@ -0,0 +1,747 @@ +{ + "formatVersion": 1, + "database": { + "version": 24, + "identityHash": "ea8559bbdf434c7b9086384a9a4cc8e6", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ea8559bbdf434c7b9086384a9a4cc8e6')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json new file mode 100644 index 0000000..01a491b --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/25.json @@ -0,0 +1,821 @@ +{ + "formatVersion": 1, + "database": { + "version": 25, + "identityHash": "e2cb844862443c2c5cc884c11f120d43", + "entities": [ + { + "tableName": "TootEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `text` TEXT, `urls` TEXT, `descriptions` TEXT, `contentWarning` TEXT, `inReplyToId` TEXT, `inReplyToText` TEXT, `inReplyToUsername` TEXT, `visibility` INTEGER, `poll` TEXT)", + "fields": [ + { + "fieldPath": "uid", + "columnName": "uid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "urls", + "columnName": "urls", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "descriptions", + "columnName": "descriptions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToText", + "columnName": "inReplyToText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToUsername", + "columnName": "inReplyToUsername", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "uid" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e2cb844862443c2c5cc884c11f120d43')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/26.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/26.json new file mode 100644 index 0000000..bd82fd4 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/26.json @@ -0,0 +1,747 @@ +{ + "formatVersion": 1, + "database": { + "version": 26, + "identityHash": "14fb3d5743b7a89e8e62463e05f086ab", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '14fb3d5743b7a89e8e62463e05f086ab')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/27.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/27.json new file mode 100644 index 0000000..c839630 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/27.json @@ -0,0 +1,753 @@ +{ + "formatVersion": 1, + "database": { + "version": 27, + "identityHash": "be914d4eb3f406b6970fef53a925afa1", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT, `visibility` INTEGER, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'be914d4eb3f406b6970fef53a925afa1')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/28.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/28.json new file mode 100644 index 0000000..c9e9b37 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/28.json @@ -0,0 +1,777 @@ +{ + "formatVersion": 1, + "database": { + "version": 28, + "identityHash": "867026e095d84652026e902709389c00", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '867026e095d84652026e902709389c00')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/29.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/29.json new file mode 100644 index 0000000..654211a --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/29.json @@ -0,0 +1,789 @@ +{ + "formatVersion": 1, + "database": { + "version": 29, + "identityHash": "62c289344334da2db091ad4ba0a49c6a", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '62c289344334da2db091ad4ba0a49c6a')" + ] + } +} diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/30.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/30.json new file mode 100644 index 0000000..e0a9e9f --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/30.json @@ -0,0 +1,807 @@ +{ + "formatVersion": 1, + "database": { + "version": 30, + "identityHash": "a75615171612bdfc9e3d4201ebf6071a", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a75615171612bdfc9e3d4201ebf6071a')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/31.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/31.json new file mode 100644 index 0000000..c705293 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/31.json @@ -0,0 +1,809 @@ +{ + "formatVersion": 1, + "database": { + "version": 31, + "identityHash": "a75615171612bdfc9e3d4201ebf6071a", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a75615171612bdfc9e3d4201ebf6071a')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json new file mode 100644 index 0000000..97ad414 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/32.json @@ -0,0 +1,815 @@ +{ + "formatVersion": 1, + "database": { + "version": 32, + "identityHash": "c92343960c9d46d9cfd49f1873cce47d", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsible` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsible", + "columnName": "s_collapsible", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c92343960c9d46d9cfd49f1873cce47d')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json new file mode 100644 index 0000000..e6d8ec7 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/33.json @@ -0,0 +1,809 @@ +{ + "formatVersion": 1, + "database": { + "version": 33, + "identityHash": "920a0e0c9a600bd236f6bf959b469c18", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '920a0e0c9a600bd236f6bf959b469c18')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/34.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/34.json new file mode 100644 index 0000000..c135469 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/34.json @@ -0,0 +1,815 @@ +{ + "formatVersion": 1, + "database": { + "version": 34, + "identityHash": "7f766d68ab5d72a7988cd81c183e9a9d", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7f766d68ab5d72a7988cd81c183e9a9d')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/35.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/35.json new file mode 100644 index 0000000..9b71adf --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/35.json @@ -0,0 +1,821 @@ +{ + "formatVersion": 1, + "database": { + "version": 35, + "identityHash": "9e6c0bb60538683a16c30fa3e1cc24f2", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9e6c0bb60538683a16c30fa3e1cc24f2')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/36.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/36.json new file mode 100644 index 0000000..d009a9e --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/36.json @@ -0,0 +1,857 @@ +{ + "formatVersion": 1, + "database": { + "version": 36, + "identityHash": "1b7461c291f67fe0b21f77b95de6a6be", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1b7461c291f67fe0b21f77b95de6a6be')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/37.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/37.json new file mode 100644 index 0000000..8d74824 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/37.json @@ -0,0 +1,869 @@ +{ + "formatVersion": 1, + "database": { + "version": 37, + "identityHash": "11033751d382aa8a1c6fc68833097d35", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '11033751d382aa8a1c6fc68833097d35')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/38.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/38.json new file mode 100644 index 0000000..391d6b8 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/38.json @@ -0,0 +1,875 @@ +{ + "formatVersion": 1, + "database": { + "version": 38, + "identityHash": "798fc8d34064eb671c079689d4650ea5", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '798fc8d34064eb671c079689d4650ea5')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/39.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/39.json new file mode 100644 index 0000000..be96b28 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/39.json @@ -0,0 +1,887 @@ +{ + "formatVersion": 1, + "database": { + "version": 39, + "identityHash": "ed3b752a3faec9d092d5ac0a2823d5d5", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'ed3b752a3faec9d092d5ac0a2823d5d5')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/40.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/40.json new file mode 100644 index 0000000..54d5a2b --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/40.json @@ -0,0 +1,929 @@ +{ + "formatVersion": 1, + "database": { + "version": 40, + "identityHash": "0423fb3f7d09db5f12023f2f4e7297b5", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '0423fb3f7d09db5f12023f2f4e7297b5')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/41.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/41.json new file mode 100644 index 0000000..2bc6256 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/41.json @@ -0,0 +1,935 @@ +{ + "formatVersion": 1, + "database": { + "version": 41, + "identityHash": "1de8f20c7f28e1f11b33e7a55137feef", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `scheduledAt` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1de8f20c7f28e1f11b33e7a55137feef')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/42.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/42.json new file mode 100644 index 0000000..a47f993 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/42.json @@ -0,0 +1,953 @@ +{ + "formatVersion": 1, + "database": { + "version": 42, + "identityHash": "a62399cb3859de7fcbb9bd7053f7cb1d", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a62399cb3859de7fcbb9bd7053f7cb1d')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/43.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/43.json new file mode 100644 index 0000000..eeceb20 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/43.json @@ -0,0 +1,959 @@ +{ + "formatVersion": 1, + "database": { + "version": 43, + "identityHash": "bf68abe55bb58765da7f9d6f7ef618e2", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'bf68abe55bb58765da7f9d6f7ef618e2')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/44.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/44.json new file mode 100644 index 0000000..e04b075 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/44.json @@ -0,0 +1,965 @@ +{ + "formatVersion": 1, + "database": { + "version": 44, + "identityHash": "7b5271980102f35e55438f46777e3d46", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReports", + "columnName": "notificationsReports", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '7b5271980102f35e55438f46777e3d46')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/45.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/45.json new file mode 100644 index 0000000..1296c0d --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/45.json @@ -0,0 +1,977 @@ +{ + "formatVersion": 1, + "database": { + "version": 45, + "identityHash": "cb4d4c0de04e945005adbb43bc534378", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReports", + "columnName": "notificationsReports", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editedAt", + "columnName": "editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.editedAt", + "columnName": "s_editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'cb4d4c0de04e945005adbb43bc534378')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/46.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/46.json new file mode 100644 index 0000000..88f47c6 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/46.json @@ -0,0 +1,983 @@ +{ + "formatVersion": 1, + "database": { + "version": 46, + "identityHash": "3cdfad61c4cf7e1ad5c70783e60e6845", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReports", + "columnName": "notificationsReports", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editedAt", + "columnName": "editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.editedAt", + "columnName": "s_editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '3cdfad61c4cf7e1ad5c70783e60e6845')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/47.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/47.json new file mode 100644 index 0000000..f0450a1 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/47.json @@ -0,0 +1,989 @@ +{ + "formatVersion": 1, + "database": { + "version": 47, + "identityHash": "496e1f2135a296e49eef88551ecbdd2c", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `failedToSendNew` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "failedToSendNew", + "columnName": "failedToSendNew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReports", + "columnName": "notificationsReports", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "instance" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editedAt", + "columnName": "editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "serverId", + "timelineUserId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.editedAt", + "columnName": "s_editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "id", + "accountId" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '496e1f2135a296e49eef88551ecbdd2c')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/48.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/48.json new file mode 100644 index 0000000..8503ae7 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/48.json @@ -0,0 +1,995 @@ +{ + "formatVersion": 1, + "database": { + "version": 48, + "identityHash": "a394ca5b45df9358fdc4d2eaae69cce3", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `failedToSendNew` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "failedToSendNew", + "columnName": "failedToSendNew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReports", + "columnName": "notificationsReports", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "instance" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, `filtered` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editedAt", + "columnName": "editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.editedAt", + "columnName": "s_editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "accountId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a394ca5b45df9358fdc4d2eaae69cce3')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/49.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/49.json new file mode 100644 index 0000000..339e372 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/49.json @@ -0,0 +1,1001 @@ +{ + "formatVersion": 1, + "database": { + "version": 49, + "identityHash": "e7085677596f03c64da3d26e05321a08", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `failedToSendNew` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "failedToSendNew", + "columnName": "failedToSendNew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `activeNotifications` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL, `lastVisibleHomeTimelineStatusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReports", + "columnName": "notificationsReports", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "activeNotifications", + "columnName": "activeNotifications", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastVisibleHomeTimelineStatusId", + "columnName": "lastVisibleHomeTimelineStatusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "instance" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, `filtered` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editedAt", + "columnName": "editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.editedAt", + "columnName": "s_editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "accountId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e7085677596f03c64da3d26e05321a08')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/50.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/50.json new file mode 100644 index 0000000..6b1a546 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/50.json @@ -0,0 +1,995 @@ +{ + "formatVersion": 1, + "database": { + "version": 50, + "identityHash": "4eaf69e915d4a15f021547b725101acd", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `failedToSendNew` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "failedToSendNew", + "columnName": "failedToSendNew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL, `lastVisibleHomeTimelineStatusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReports", + "columnName": "notificationsReports", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastVisibleHomeTimelineStatusId", + "columnName": "lastVisibleHomeTimelineStatusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "instance" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, `filtered` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editedAt", + "columnName": "editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.editedAt", + "columnName": "s_editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "accountId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '4eaf69e915d4a15f021547b725101acd')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/51.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/51.json new file mode 100644 index 0000000..32c5eee --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/51.json @@ -0,0 +1,1002 @@ +{ + "formatVersion": 1, + "database": { + "version": 51, + "identityHash": "446158bf571fbd08787628bb829fa3c0", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `failedToSendNew` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "failedToSendNew", + "columnName": "failedToSendNew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `notificationMarkerId` TEXT NOT NULL DEFAULT '0', `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL, `lastVisibleHomeTimelineStatusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReports", + "columnName": "notificationsReports", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationMarkerId", + "columnName": "notificationMarkerId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'0'" + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastVisibleHomeTimelineStatusId", + "columnName": "lastVisibleHomeTimelineStatusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "instance" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, `filtered` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editedAt", + "columnName": "editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.editedAt", + "columnName": "s_editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "accountId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '446158bf571fbd08787628bb829fa3c0')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/52.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/52.json new file mode 100644 index 0000000..18290c9 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/52.json @@ -0,0 +1,1009 @@ +{ + "formatVersion": 1, + "database": { + "version": 52, + "identityHash": "233a8680f540e9a89950da21532ce85d", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `failedToSendNew` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "failedToSendNew", + "columnName": "failedToSendNew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `notificationMarkerId` TEXT NOT NULL DEFAULT '0', `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL, `lastVisibleHomeTimelineStatusId` TEXT, `locked` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReports", + "columnName": "notificationsReports", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationMarkerId", + "columnName": "notificationMarkerId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'0'" + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastVisibleHomeTimelineStatusId", + "columnName": "lastVisibleHomeTimelineStatusId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "instance" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, `filtered` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editedAt", + "columnName": "editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.editedAt", + "columnName": "s_editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "accountId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '233a8680f540e9a89950da21532ce85d')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/53.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/53.json new file mode 100644 index 0000000..824768c --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/53.json @@ -0,0 +1,1009 @@ +{ + "formatVersion": 1, + "database": { + "version": 53, + "identityHash": "233a8680f540e9a89950da21532ce85d", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `failedToSendNew` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "failedToSendNew", + "columnName": "failedToSendNew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `notificationMarkerId` TEXT NOT NULL DEFAULT '0', `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL, `lastVisibleHomeTimelineStatusId` TEXT, `locked` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReports", + "columnName": "notificationsReports", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationMarkerId", + "columnName": "notificationMarkerId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'0'" + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastVisibleHomeTimelineStatusId", + "columnName": "lastVisibleHomeTimelineStatusId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "instance" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, `filtered` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editedAt", + "columnName": "editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.editedAt", + "columnName": "s_editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "accountId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '233a8680f540e9a89950da21532ce85d')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/54.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/54.json new file mode 100644 index 0000000..a02b302 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/54.json @@ -0,0 +1,1016 @@ +{ + "formatVersion": 1, + "database": { + "version": 54, + "identityHash": "c86c3e5ef2c1c5903657a0138b4b2520", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `failedToSendNew` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "failedToSendNew", + "columnName": "failedToSendNew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `notificationMarkerId` TEXT NOT NULL DEFAULT '0', `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL, `lastVisibleHomeTimelineStatusId` TEXT, `locked` INTEGER NOT NULL DEFAULT 0, `hasDirectMessageBadge` INTEGER NOT NULL DEFAULT 0)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReports", + "columnName": "notificationsReports", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationMarkerId", + "columnName": "notificationMarkerId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'0'" + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastVisibleHomeTimelineStatusId", + "columnName": "lastVisibleHomeTimelineStatusId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hasDirectMessageBadge", + "columnName": "hasDirectMessageBadge", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "instance" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, `filtered` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editedAt", + "columnName": "editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.editedAt", + "columnName": "s_editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "accountId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'c86c3e5ef2c1c5903657a0138b4b2520')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/56.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/56.json new file mode 100644 index 0000000..a9f974e --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/56.json @@ -0,0 +1,1034 @@ +{ + "formatVersion": 1, + "database": { + "version": 56, + "identityHash": "1d1eba6d905d2a6e16ae2daa81c0ab4a", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `failedToSendNew` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "failedToSendNew", + "columnName": "failedToSendNew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `notificationMarkerId` TEXT NOT NULL DEFAULT '0', `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL, `lastVisibleHomeTimelineStatusId` TEXT, `locked` INTEGER NOT NULL DEFAULT 0, `hasDirectMessageBadge` INTEGER NOT NULL DEFAULT 0, `isShowHomeBoosts` INTEGER NOT NULL, `isShowHomeReplies` INTEGER NOT NULL, `isShowHomeSelfBoosts` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReports", + "columnName": "notificationsReports", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationMarkerId", + "columnName": "notificationMarkerId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'0'" + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastVisibleHomeTimelineStatusId", + "columnName": "lastVisibleHomeTimelineStatusId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hasDirectMessageBadge", + "columnName": "hasDirectMessageBadge", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isShowHomeBoosts", + "columnName": "isShowHomeBoosts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShowHomeReplies", + "columnName": "isShowHomeReplies", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShowHomeSelfBoosts", + "columnName": "isShowHomeSelfBoosts", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "instance" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, `filtered` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editedAt", + "columnName": "editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.editedAt", + "columnName": "s_editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "accountId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1d1eba6d905d2a6e16ae2daa81c0ab4a')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/58.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/58.json new file mode 100644 index 0000000..65aa67d --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/58.json @@ -0,0 +1,1040 @@ +{ + "formatVersion": 1, + "database": { + "version": 58, + "identityHash": "1d0e1cdf0b4c3f787333b9abf3b2b26a", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `failedToSendNew` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "failedToSendNew", + "columnName": "failedToSendNew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `notificationMarkerId` TEXT NOT NULL DEFAULT '0', `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL, `lastVisibleHomeTimelineStatusId` TEXT, `locked` INTEGER NOT NULL DEFAULT 0, `hasDirectMessageBadge` INTEGER NOT NULL DEFAULT 0, `isShowHomeBoosts` INTEGER NOT NULL, `isShowHomeReplies` INTEGER NOT NULL, `isShowHomeSelfBoosts` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReports", + "columnName": "notificationsReports", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationMarkerId", + "columnName": "notificationMarkerId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'0'" + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastVisibleHomeTimelineStatusId", + "columnName": "lastVisibleHomeTimelineStatusId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hasDirectMessageBadge", + "columnName": "hasDirectMessageBadge", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isShowHomeBoosts", + "columnName": "isShowHomeBoosts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShowHomeReplies", + "columnName": "isShowHomeReplies", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShowHomeSelfBoosts", + "columnName": "isShowHomeSelfBoosts", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, `translationEnabled` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "translationEnabled", + "columnName": "translationEnabled", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "instance" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `timelineUserId` INTEGER NOT NULL, `authorServerId` TEXT, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT, `mentions` TEXT, `tags` TEXT, `application` TEXT, `reblogServerId` TEXT, `reblogAccountId` TEXT, `poll` TEXT, `muted` INTEGER, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, `filtered` TEXT, PRIMARY KEY(`serverId`, `timelineUserId`), FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editedAt", + "columnName": "editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogServerId", + "columnName": "reblogServerId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_timelineUserId", + "unique": false, + "columnNames": [ + "authorServerId", + "timelineUserId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId` ON `${TABLE_NAME}` (`authorServerId`, `timelineUserId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "timelineUserId" + ], + "referencedColumns": [ + "serverId", + "timelineUserId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `timelineUserId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `timelineUserId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timelineUserId", + "columnName": "timelineUserId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "timelineUserId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.editedAt", + "columnName": "s_editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "accountId" + ] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1d0e1cdf0b4c3f787333b9abf3b2b26a')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/60.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/60.json new file mode 100644 index 0000000..03fc701 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/60.json @@ -0,0 +1,1325 @@ +{ + "formatVersion": 1, + "database": { + "version": 60, + "identityHash": "1f8ec0c172cc1cae16313d737f6f8e34", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `failedToSendNew` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "failedToSendNew", + "columnName": "failedToSendNew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `notificationMarkerId` TEXT NOT NULL DEFAULT '0', `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL, `lastVisibleHomeTimelineStatusId` TEXT, `locked` INTEGER NOT NULL DEFAULT 0, `hasDirectMessageBadge` INTEGER NOT NULL DEFAULT 0, `isShowHomeBoosts` INTEGER NOT NULL, `isShowHomeReplies` INTEGER NOT NULL, `isShowHomeSelfBoosts` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReports", + "columnName": "notificationsReports", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationMarkerId", + "columnName": "notificationMarkerId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'0'" + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastVisibleHomeTimelineStatusId", + "columnName": "lastVisibleHomeTimelineStatusId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hasDirectMessageBadge", + "columnName": "hasDirectMessageBadge", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isShowHomeBoosts", + "columnName": "isShowHomeBoosts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShowHomeReplies", + "columnName": "isShowHomeReplies", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShowHomeSelfBoosts", + "columnName": "isShowHomeSelfBoosts", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, `translationEnabled` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "translationEnabled", + "columnName": "translationEnabled", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "instance" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `tuskyAccountId` INTEGER NOT NULL, `authorServerId` TEXT NOT NULL, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT NOT NULL, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `mentions` TEXT NOT NULL, `tags` TEXT NOT NULL, `application` TEXT, `poll` TEXT, `muted` INTEGER NOT NULL, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, `filtered` TEXT NOT NULL, PRIMARY KEY(`serverId`, `tuskyAccountId`), FOREIGN KEY(`authorServerId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tuskyAccountId", + "columnName": "tuskyAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editedAt", + "columnName": "editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "tuskyAccountId" + ] + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_tuskyAccountId", + "unique": false, + "columnNames": [ + "authorServerId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_tuskyAccountId` ON `${TABLE_NAME}` (`authorServerId`, `tuskyAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `tuskyAccountId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `tuskyAccountId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tuskyAccountId", + "columnName": "tuskyAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "tuskyAccountId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.editedAt", + "columnName": "s_editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "accountId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "NotificationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tuskyAccountId` INTEGER NOT NULL, `type` TEXT, `id` TEXT NOT NULL, `accountId` TEXT, `statusId` TEXT, `reportId` TEXT, `loading` INTEGER NOT NULL, PRIMARY KEY(`id`, `tuskyAccountId`), FOREIGN KEY(`accountId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`statusId`, `tuskyAccountId`) REFERENCES `TimelineStatusEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`reportId`, `tuskyAccountId`) REFERENCES `NotificationReportEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "tuskyAccountId", + "columnName": "tuskyAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reportId", + "columnName": "reportId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loading", + "columnName": "loading", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "tuskyAccountId" + ] + }, + "indices": [ + { + "name": "index_NotificationEntity_accountId_tuskyAccountId", + "unique": false, + "columnNames": [ + "accountId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_accountId_tuskyAccountId` ON `${TABLE_NAME}` (`accountId`, `tuskyAccountId`)" + }, + { + "name": "index_NotificationEntity_statusId_tuskyAccountId", + "unique": false, + "columnNames": [ + "statusId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_statusId_tuskyAccountId` ON `${TABLE_NAME}` (`statusId`, `tuskyAccountId`)" + }, + { + "name": "index_NotificationEntity_reportId_tuskyAccountId", + "unique": false, + "columnNames": [ + "reportId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_reportId_tuskyAccountId` ON `${TABLE_NAME}` (`reportId`, `tuskyAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "accountId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + }, + { + "table": "TimelineStatusEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "statusId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + }, + { + "table": "NotificationReportEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "reportId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + } + ] + }, + { + "tableName": "NotificationReportEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tuskyAccountId` INTEGER NOT NULL, `serverId` TEXT NOT NULL, `category` TEXT NOT NULL, `statusIds` TEXT, `createdAt` INTEGER NOT NULL, `targetAccountId` TEXT, PRIMARY KEY(`serverId`, `tuskyAccountId`), FOREIGN KEY(`targetAccountId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "tuskyAccountId", + "columnName": "tuskyAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "statusIds", + "columnName": "statusIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "targetAccountId", + "columnName": "targetAccountId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "tuskyAccountId" + ] + }, + "indices": [ + { + "name": "index_NotificationReportEntity_targetAccountId_tuskyAccountId", + "unique": false, + "columnNames": [ + "targetAccountId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationReportEntity_targetAccountId_tuskyAccountId` ON `${TABLE_NAME}` (`targetAccountId`, `tuskyAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "targetAccountId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + } + ] + }, + { + "tableName": "HomeTimelineEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tuskyAccountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `statusId` TEXT, `reblogAccountId` TEXT, `loading` INTEGER NOT NULL, PRIMARY KEY(`id`, `tuskyAccountId`), FOREIGN KEY(`statusId`, `tuskyAccountId`) REFERENCES `TimelineStatusEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`reblogAccountId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "tuskyAccountId", + "columnName": "tuskyAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loading", + "columnName": "loading", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "tuskyAccountId" + ] + }, + "indices": [ + { + "name": "index_HomeTimelineEntity_statusId_tuskyAccountId", + "unique": false, + "columnNames": [ + "statusId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_HomeTimelineEntity_statusId_tuskyAccountId` ON `${TABLE_NAME}` (`statusId`, `tuskyAccountId`)" + }, + { + "name": "index_HomeTimelineEntity_reblogAccountId_tuskyAccountId", + "unique": false, + "columnNames": [ + "reblogAccountId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_HomeTimelineEntity_reblogAccountId_tuskyAccountId` ON `${TABLE_NAME}` (`reblogAccountId`, `tuskyAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineStatusEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "statusId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + }, + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "reblogAccountId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '1f8ec0c172cc1cae16313d737f6f8e34')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/com.keylesspalace.tusky.db.AppDatabase/62.json b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/62.json new file mode 100644 index 0000000..740ab36 --- /dev/null +++ b/app/schemas/com.keylesspalace.tusky.db.AppDatabase/62.json @@ -0,0 +1,1331 @@ +{ + "formatVersion": 1, + "database": { + "version": 62, + "identityHash": "f50579baaea33d99c59a34671799682a", + "entities": [ + { + "tableName": "DraftEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountId` INTEGER NOT NULL, `inReplyToId` TEXT, `content` TEXT, `contentWarning` TEXT, `sensitive` INTEGER NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `poll` TEXT, `failedToSend` INTEGER NOT NULL, `failedToSendNew` INTEGER NOT NULL, `scheduledAt` TEXT, `language` TEXT, `statusId` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "contentWarning", + "columnName": "contentWarning", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "failedToSend", + "columnName": "failedToSend", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "failedToSendNew", + "columnName": "failedToSendNew", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "AccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, `clientId` TEXT, `clientSecret` TEXT, `isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `profilePictureUrl` TEXT NOT NULL, `notificationsEnabled` INTEGER NOT NULL, `notificationsMentioned` INTEGER NOT NULL, `notificationsFollowed` INTEGER NOT NULL, `notificationsFollowRequested` INTEGER NOT NULL, `notificationsReblogged` INTEGER NOT NULL, `notificationsFavorited` INTEGER NOT NULL, `notificationsPolls` INTEGER NOT NULL, `notificationsSubscriptions` INTEGER NOT NULL, `notificationsSignUps` INTEGER NOT NULL, `notificationsUpdates` INTEGER NOT NULL, `notificationsReports` INTEGER NOT NULL, `notificationSound` INTEGER NOT NULL, `notificationVibration` INTEGER NOT NULL, `notificationLight` INTEGER NOT NULL, `defaultPostPrivacy` INTEGER NOT NULL, `defaultReplyPrivacy` INTEGER NOT NULL, `defaultMediaSensitivity` INTEGER NOT NULL, `defaultPostLanguage` TEXT NOT NULL, `alwaysShowSensitiveMedia` INTEGER NOT NULL, `alwaysOpenSpoiler` INTEGER NOT NULL, `mediaPreviewEnabled` INTEGER NOT NULL, `lastNotificationId` TEXT NOT NULL, `notificationMarkerId` TEXT NOT NULL DEFAULT '0', `emojis` TEXT NOT NULL, `tabPreferences` TEXT NOT NULL, `notificationsFilter` TEXT NOT NULL, `oauthScopes` TEXT NOT NULL, `unifiedPushUrl` TEXT NOT NULL, `pushPubKey` TEXT NOT NULL, `pushPrivKey` TEXT NOT NULL, `pushAuth` TEXT NOT NULL, `pushServerKey` TEXT NOT NULL, `lastVisibleHomeTimelineStatusId` TEXT, `locked` INTEGER NOT NULL DEFAULT 0, `hasDirectMessageBadge` INTEGER NOT NULL DEFAULT 0, `isShowHomeBoosts` INTEGER NOT NULL, `isShowHomeReplies` INTEGER NOT NULL, `isShowHomeSelfBoosts` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "domain", + "columnName": "domain", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accessToken", + "columnName": "accessToken", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "clientId", + "columnName": "clientId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "clientSecret", + "columnName": "clientSecret", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "isActive", + "columnName": "isActive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "profilePictureUrl", + "columnName": "profilePictureUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsEnabled", + "columnName": "notificationsEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsMentioned", + "columnName": "notificationsMentioned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowed", + "columnName": "notificationsFollowed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFollowRequested", + "columnName": "notificationsFollowRequested", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReblogged", + "columnName": "notificationsReblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsFavorited", + "columnName": "notificationsFavorited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsPolls", + "columnName": "notificationsPolls", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSubscriptions", + "columnName": "notificationsSubscriptions", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsSignUps", + "columnName": "notificationsSignUps", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsUpdates", + "columnName": "notificationsUpdates", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationsReports", + "columnName": "notificationsReports", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationSound", + "columnName": "notificationSound", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationVibration", + "columnName": "notificationVibration", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "notificationLight", + "columnName": "notificationLight", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostPrivacy", + "columnName": "defaultPostPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultReplyPrivacy", + "columnName": "defaultReplyPrivacy", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultMediaSensitivity", + "columnName": "defaultMediaSensitivity", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "defaultPostLanguage", + "columnName": "defaultPostLanguage", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "alwaysShowSensitiveMedia", + "columnName": "alwaysShowSensitiveMedia", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "alwaysOpenSpoiler", + "columnName": "alwaysOpenSpoiler", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "mediaPreviewEnabled", + "columnName": "mediaPreviewEnabled", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastNotificationId", + "columnName": "lastNotificationId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationMarkerId", + "columnName": "notificationMarkerId", + "affinity": "TEXT", + "notNull": true, + "defaultValue": "'0'" + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tabPreferences", + "columnName": "tabPreferences", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "notificationsFilter", + "columnName": "notificationsFilter", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "oauthScopes", + "columnName": "oauthScopes", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unifiedPushUrl", + "columnName": "unifiedPushUrl", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPubKey", + "columnName": "pushPubKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushPrivKey", + "columnName": "pushPrivKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushAuth", + "columnName": "pushAuth", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "pushServerKey", + "columnName": "pushServerKey", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastVisibleHomeTimelineStatusId", + "columnName": "lastVisibleHomeTimelineStatusId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "hasDirectMessageBadge", + "columnName": "hasDirectMessageBadge", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "0" + }, + { + "fieldPath": "isShowHomeBoosts", + "columnName": "isShowHomeBoosts", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShowHomeReplies", + "columnName": "isShowHomeReplies", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isShowHomeSelfBoosts", + "columnName": "isShowHomeSelfBoosts", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + }, + "indices": [ + { + "name": "index_AccountEntity_domain_accountId", + "unique": true, + "columnNames": [ + "domain", + "accountId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_AccountEntity_domain_accountId` ON `${TABLE_NAME}` (`domain`, `accountId`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "InstanceEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, `maxPollOptions` INTEGER, `maxPollOptionLength` INTEGER, `minPollDuration` INTEGER, `maxPollDuration` INTEGER, `charactersReservedPerUrl` INTEGER, `version` TEXT, `videoSizeLimit` INTEGER, `imageSizeLimit` INTEGER, `imageMatrixLimit` INTEGER, `maxMediaAttachments` INTEGER, `maxFields` INTEGER, `maxFieldNameLength` INTEGER, `maxFieldValueLength` INTEGER, `translationEnabled` INTEGER, PRIMARY KEY(`instance`))", + "fields": [ + { + "fieldPath": "instance", + "columnName": "instance", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojiList", + "columnName": "emojiList", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "maximumTootCharacters", + "columnName": "maximumTootCharacters", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptions", + "columnName": "maxPollOptions", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollOptionLength", + "columnName": "maxPollOptionLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "minPollDuration", + "columnName": "minPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxPollDuration", + "columnName": "maxPollDuration", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "charactersReservedPerUrl", + "columnName": "charactersReservedPerUrl", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "version", + "columnName": "version", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "videoSizeLimit", + "columnName": "videoSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageSizeLimit", + "columnName": "imageSizeLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "imageMatrixLimit", + "columnName": "imageMatrixLimit", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxMediaAttachments", + "columnName": "maxMediaAttachments", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFields", + "columnName": "maxFields", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldNameLength", + "columnName": "maxFieldNameLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "maxFieldValueLength", + "columnName": "maxFieldValueLength", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "translationEnabled", + "columnName": "translationEnabled", + "affinity": "INTEGER", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "instance" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "TimelineStatusEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `url` TEXT, `tuskyAccountId` INTEGER NOT NULL, `authorServerId` TEXT NOT NULL, `inReplyToId` TEXT, `inReplyToAccountId` TEXT, `content` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `editedAt` INTEGER, `emojis` TEXT NOT NULL, `reblogsCount` INTEGER NOT NULL, `favouritesCount` INTEGER NOT NULL, `repliesCount` INTEGER NOT NULL, `reblogged` INTEGER NOT NULL, `bookmarked` INTEGER NOT NULL, `favourited` INTEGER NOT NULL, `sensitive` INTEGER NOT NULL, `spoilerText` TEXT NOT NULL, `visibility` INTEGER NOT NULL, `attachments` TEXT NOT NULL, `mentions` TEXT NOT NULL, `tags` TEXT NOT NULL, `application` TEXT, `poll` TEXT, `muted` INTEGER NOT NULL, `expanded` INTEGER NOT NULL, `contentCollapsed` INTEGER NOT NULL, `contentShowing` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, `card` TEXT, `language` TEXT, `filtered` TEXT NOT NULL, PRIMARY KEY(`serverId`, `tuskyAccountId`), FOREIGN KEY(`authorServerId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "tuskyAccountId", + "columnName": "tuskyAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "authorServerId", + "columnName": "authorServerId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "inReplyToId", + "columnName": "inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "inReplyToAccountId", + "columnName": "inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "content", + "columnName": "content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "editedAt", + "columnName": "editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "reblogsCount", + "columnName": "reblogsCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favouritesCount", + "columnName": "favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "repliesCount", + "columnName": "repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reblogged", + "columnName": "reblogged", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "bookmarked", + "columnName": "bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "favourited", + "columnName": "favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sensitive", + "columnName": "sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "spoilerText", + "columnName": "spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "visibility", + "columnName": "visibility", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "attachments", + "columnName": "attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mentions", + "columnName": "mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "application", + "columnName": "application", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "poll", + "columnName": "poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "muted", + "columnName": "muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "expanded", + "columnName": "expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentCollapsed", + "columnName": "contentCollapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "contentShowing", + "columnName": "contentShowing", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pinned", + "columnName": "pinned", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "card", + "columnName": "card", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "filtered", + "columnName": "filtered", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "tuskyAccountId" + ] + }, + "indices": [ + { + "name": "index_TimelineStatusEntity_authorServerId_tuskyAccountId", + "unique": false, + "columnNames": [ + "authorServerId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_tuskyAccountId` ON `${TABLE_NAME}` (`authorServerId`, `tuskyAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "authorServerId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + } + ] + }, + { + "tableName": "TimelineAccountEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`serverId` TEXT NOT NULL, `tuskyAccountId` INTEGER NOT NULL, `localUsername` TEXT NOT NULL, `username` TEXT NOT NULL, `displayName` TEXT NOT NULL, `url` TEXT NOT NULL, `avatar` TEXT NOT NULL, `emojis` TEXT NOT NULL, `bot` INTEGER NOT NULL, PRIMARY KEY(`serverId`, `tuskyAccountId`))", + "fields": [ + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "tuskyAccountId", + "columnName": "tuskyAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "localUsername", + "columnName": "localUsername", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "username", + "columnName": "username", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "displayName", + "columnName": "displayName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "avatar", + "columnName": "avatar", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "emojis", + "columnName": "emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "bot", + "columnName": "bot", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "tuskyAccountId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "ConversationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`accountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `order` INTEGER NOT NULL, `accounts` TEXT NOT NULL, `unread` INTEGER NOT NULL, `s_id` TEXT NOT NULL, `s_url` TEXT, `s_inReplyToId` TEXT, `s_inReplyToAccountId` TEXT, `s_account` TEXT NOT NULL, `s_content` TEXT NOT NULL, `s_createdAt` INTEGER NOT NULL, `s_editedAt` INTEGER, `s_emojis` TEXT NOT NULL, `s_favouritesCount` INTEGER NOT NULL, `s_repliesCount` INTEGER NOT NULL, `s_favourited` INTEGER NOT NULL, `s_bookmarked` INTEGER NOT NULL, `s_sensitive` INTEGER NOT NULL, `s_spoilerText` TEXT NOT NULL, `s_attachments` TEXT NOT NULL, `s_mentions` TEXT NOT NULL, `s_tags` TEXT, `s_showingHiddenContent` INTEGER NOT NULL, `s_expanded` INTEGER NOT NULL, `s_collapsed` INTEGER NOT NULL, `s_muted` INTEGER NOT NULL, `s_poll` TEXT, `s_language` TEXT, PRIMARY KEY(`id`, `accountId`))", + "fields": [ + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accounts", + "columnName": "accounts", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unread", + "columnName": "unread", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.id", + "columnName": "s_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.url", + "columnName": "s_url", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToId", + "columnName": "s_inReplyToId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.inReplyToAccountId", + "columnName": "s_inReplyToAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.account", + "columnName": "s_account", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.content", + "columnName": "s_content", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.createdAt", + "columnName": "s_createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.editedAt", + "columnName": "s_editedAt", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "lastStatus.emojis", + "columnName": "s_emojis", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.favouritesCount", + "columnName": "s_favouritesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.repliesCount", + "columnName": "s_repliesCount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.favourited", + "columnName": "s_favourited", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.bookmarked", + "columnName": "s_bookmarked", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.sensitive", + "columnName": "s_sensitive", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.spoilerText", + "columnName": "s_spoilerText", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.attachments", + "columnName": "s_attachments", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.mentions", + "columnName": "s_mentions", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "lastStatus.tags", + "columnName": "s_tags", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.showingHiddenContent", + "columnName": "s_showingHiddenContent", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.expanded", + "columnName": "s_expanded", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.collapsed", + "columnName": "s_collapsed", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.muted", + "columnName": "s_muted", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "lastStatus.poll", + "columnName": "s_poll", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "lastStatus.language", + "columnName": "s_language", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "accountId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "NotificationEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tuskyAccountId` INTEGER NOT NULL, `type` TEXT, `id` TEXT NOT NULL, `accountId` TEXT, `statusId` TEXT, `reportId` TEXT, `loading` INTEGER NOT NULL, PRIMARY KEY(`id`, `tuskyAccountId`), FOREIGN KEY(`accountId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`statusId`, `tuskyAccountId`) REFERENCES `TimelineStatusEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`reportId`, `tuskyAccountId`) REFERENCES `NotificationReportEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "tuskyAccountId", + "columnName": "tuskyAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "accountId", + "columnName": "accountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reportId", + "columnName": "reportId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loading", + "columnName": "loading", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "tuskyAccountId" + ] + }, + "indices": [ + { + "name": "index_NotificationEntity_accountId_tuskyAccountId", + "unique": false, + "columnNames": [ + "accountId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_accountId_tuskyAccountId` ON `${TABLE_NAME}` (`accountId`, `tuskyAccountId`)" + }, + { + "name": "index_NotificationEntity_statusId_tuskyAccountId", + "unique": false, + "columnNames": [ + "statusId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_statusId_tuskyAccountId` ON `${TABLE_NAME}` (`statusId`, `tuskyAccountId`)" + }, + { + "name": "index_NotificationEntity_reportId_tuskyAccountId", + "unique": false, + "columnNames": [ + "reportId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_reportId_tuskyAccountId` ON `${TABLE_NAME}` (`reportId`, `tuskyAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "accountId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + }, + { + "table": "TimelineStatusEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "statusId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + }, + { + "table": "NotificationReportEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "reportId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + } + ] + }, + { + "tableName": "NotificationReportEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tuskyAccountId` INTEGER NOT NULL, `serverId` TEXT NOT NULL, `category` TEXT NOT NULL, `statusIds` TEXT, `createdAt` INTEGER NOT NULL, `targetAccountId` TEXT, PRIMARY KEY(`serverId`, `tuskyAccountId`), FOREIGN KEY(`targetAccountId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "tuskyAccountId", + "columnName": "tuskyAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "serverId", + "columnName": "serverId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "category", + "columnName": "category", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "statusIds", + "columnName": "statusIds", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "createdAt", + "columnName": "createdAt", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "targetAccountId", + "columnName": "targetAccountId", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "serverId", + "tuskyAccountId" + ] + }, + "indices": [ + { + "name": "index_NotificationReportEntity_targetAccountId_tuskyAccountId", + "unique": false, + "columnNames": [ + "targetAccountId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_NotificationReportEntity_targetAccountId_tuskyAccountId` ON `${TABLE_NAME}` (`targetAccountId`, `tuskyAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "targetAccountId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + } + ] + }, + { + "tableName": "HomeTimelineEntity", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tuskyAccountId` INTEGER NOT NULL, `id` TEXT NOT NULL, `statusId` TEXT, `reblogAccountId` TEXT, `loading` INTEGER NOT NULL, PRIMARY KEY(`id`, `tuskyAccountId`), FOREIGN KEY(`statusId`, `tuskyAccountId`) REFERENCES `TimelineStatusEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION , FOREIGN KEY(`reblogAccountId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION )", + "fields": [ + { + "fieldPath": "tuskyAccountId", + "columnName": "tuskyAccountId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "statusId", + "columnName": "statusId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "reblogAccountId", + "columnName": "reblogAccountId", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "loading", + "columnName": "loading", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "id", + "tuskyAccountId" + ] + }, + "indices": [ + { + "name": "index_HomeTimelineEntity_statusId_tuskyAccountId", + "unique": false, + "columnNames": [ + "statusId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_HomeTimelineEntity_statusId_tuskyAccountId` ON `${TABLE_NAME}` (`statusId`, `tuskyAccountId`)" + }, + { + "name": "index_HomeTimelineEntity_reblogAccountId_tuskyAccountId", + "unique": false, + "columnNames": [ + "reblogAccountId", + "tuskyAccountId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_HomeTimelineEntity_reblogAccountId_tuskyAccountId` ON `${TABLE_NAME}` (`reblogAccountId`, `tuskyAccountId`)" + } + ], + "foreignKeys": [ + { + "table": "TimelineStatusEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "statusId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + }, + { + "table": "TimelineAccountEntity", + "onDelete": "NO ACTION", + "onUpdate": "NO ACTION", + "columns": [ + "reblogAccountId", + "tuskyAccountId" + ], + "referencedColumns": [ + "serverId", + "tuskyAccountId" + ] + } + ] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f50579baaea33d99c59a34671799682a')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/keylesspalace/tusky/MigrationsTest.kt b/app/src/androidTest/java/com/keylesspalace/tusky/MigrationsTest.kt new file mode 100644 index 0000000..313ab8e --- /dev/null +++ b/app/src/androidTest/java/com/keylesspalace/tusky/MigrationsTest.kt @@ -0,0 +1,66 @@ +package com.keylesspalace.tusky + +import androidx.room.testing.MigrationTestHelper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.keylesspalace.tusky.db.AppDatabase +import org.junit.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +const val TEST_DB = "migration_test" + +@RunWith(AndroidJUnit4::class) +class MigrationsTest { + + @JvmField + @Rule + var helper: MigrationTestHelper = MigrationTestHelper( + InstrumentationRegistry.getInstrumentation(), + AppDatabase::class.java + ) + + @Test + fun migrateTo11() { + val db = helper.createDatabase(TEST_DB, 10) + + val id = 1 + val domain = "domain.site" + val token = "token" + val active = true + val accountId = "accountId" + val username = "username" + val values = arrayOf( + id, domain, token, active, accountId, username, "Display Name", + "https://picture.url", true, true, true, true, true, true, true, + true, "1000", "[]", "[{\"shortcode\": \"emoji\", \"url\": \"yes\"}]", 0, false, + false, true + ) + + db.execSQL( + "INSERT OR REPLACE INTO `AccountEntity`(`id`,`domain`,`accessToken`,`isActive`," + + "`accountId`,`username`,`displayName`,`profilePictureUrl`,`notificationsEnabled`," + + "`notificationsMentioned`,`notificationsFollowed`,`notificationsReblogged`," + + "`notificationsFavorited`,`notificationSound`,`notificationVibration`," + + "`notificationLight`,`lastNotificationId`,`activeNotifications`,`emojis`," + + "`defaultPostPrivacy`,`defaultMediaSensitivity`,`alwaysShowSensitiveMedia`," + + "`mediaPreviewEnabled`) " + + "VALUES (nullif(?, 0),?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", + values + ) + + db.close() + + val newDb = helper.runMigrationsAndValidate(TEST_DB, 11, true, AppDatabase.MIGRATION_10_11) + + val cursor = newDb.query("SELECT * FROM AccountEntity") + cursor.moveToFirst() + assertEquals(id, cursor.getInt(0)) + assertEquals(domain, cursor.getString(1)) + assertEquals(token, cursor.getString(2)) + assertEquals(active, cursor.getInt(3) != 0) + assertEquals(accountId, cursor.getString(4)) + assertEquals(username, cursor.getString(5)) + } +} diff --git a/app/src/blue/res/mipmap-hdpi/ic_launcher.png b/app/src/blue/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..023b25f Binary files /dev/null and b/app/src/blue/res/mipmap-hdpi/ic_launcher.png differ diff --git a/app/src/blue/res/mipmap-mdpi/ic_launcher.png b/app/src/blue/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..3463d7b Binary files /dev/null and b/app/src/blue/res/mipmap-mdpi/ic_launcher.png differ diff --git a/app/src/blue/res/mipmap-xhdpi/ic_launcher.png b/app/src/blue/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..3694d98 Binary files /dev/null and b/app/src/blue/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/app/src/blue/res/mipmap-xxhdpi/ic_launcher.png b/app/src/blue/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..2ef6e08 Binary files /dev/null and b/app/src/blue/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/app/src/blue/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/blue/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..8879519 Binary files /dev/null and b/app/src/blue/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/app/src/green/res/mipmap-hdpi/ic_launcher.webp b/app/src/green/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..231a158 Binary files /dev/null and b/app/src/green/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/green/res/mipmap-mdpi/ic_launcher.webp b/app/src/green/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..f792c86 Binary files /dev/null and b/app/src/green/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/green/res/mipmap-xhdpi/ic_launcher.webp b/app/src/green/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..4631f6d Binary files /dev/null and b/app/src/green/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/green/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/green/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa0e7d5 Binary files /dev/null and b/app/src/green/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/green/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/green/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..2823f33 Binary files /dev/null and b/app/src/green/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/green/res/values/flavor-colors.xml b/app/src/green/res/values/flavor-colors.xml new file mode 100644 index 0000000..a5120ea --- /dev/null +++ b/app/src/green/res/values/flavor-colors.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <color name="notification_color">#19A341</color> + + <color name="icon_background">#097b44</color> + <color name="icon_highlight">#39ff9e</color> +</resources> \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..d2c9bc4 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,219 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" > + + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="28" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/> + <uses-permission android:name="android.permission.VIBRATE" /> <!-- For notifications --> + <uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> + + <application + android:name=".TuskyApplication" + android:appCategory="social" + android:allowBackup="false" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:supportsRtl="true" + android:theme="@style/TuskyTheme" + android:usesCleartextTraffic="false" + android:localeConfig="@xml/locales_config" + android:enableOnBackInvokedCallback="true"> + + <activity + android:name=".components.login.LoginActivity" + android:windowSoftInputMode="adjustResize" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.VIEW" /> + + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.BROWSABLE" /> + + <data + android:host="${applicationId}" + android:scheme="@string/oauth_scheme" /> + </intent-filter> + </activity> + <activity android:name=".components.login.LoginWebViewActivity" /> + <activity + android:name=".MainActivity" + android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" + android:exported="true" + android:theme="@style/SplashTheme"> + + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.SEND" /> + + <category android:name="android.intent.category.DEFAULT" /> + + <data android:mimeType="text/plain" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.SEND" /> + + <category android:name="android.intent.category.DEFAULT" /> + + <data android:mimeType="image/*" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.SEND" /> + + <category android:name="android.intent.category.DEFAULT" /> + + <data android:mimeType="video/*" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.SEND_MULTIPLE" /> + + <category android:name="android.intent.category.DEFAULT" /> + + <data android:mimeType="image/*" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.SEND_MULTIPLE" /> + + <category android:name="android.intent.category.DEFAULT" /> + + <data android:mimeType="video/*" /> + </intent-filter> + <intent-filter> + <action android:name="android.intent.action.SEND" /> + + <category android:name="android.intent.category.DEFAULT" /> + + <data android:mimeType="audio/*" /> + </intent-filter> + + <meta-data + android:name="android.app.shortcuts" + android:resource="@xml/share_shortcuts" /> + <meta-data + android:name="android.service.chooser.chooser_target_service" + android:value="androidx.sharetarget.ChooserTargetServiceCompat" /> + + </activity> + <activity + android:name=".components.compose.ComposeActivity" + android:theme="@style/TuskyDialogActivityTheme" + android:windowSoftInputMode="stateVisible|adjustResize" /> + <activity + android:name=".components.viewthread.ViewThreadActivity" + android:configChanges="orientation|screenSize" /> + <activity + android:name=".ViewMediaActivity" + android:theme="@style/TuskyBaseTheme" + android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" /> + <activity + android:name=".components.account.AccountActivity" + android:configChanges="orientation|screenSize|keyboardHidden|screenLayout|smallestScreenSize" /> + <activity android:name=".EditProfileActivity" /> + <activity android:name=".components.preference.PreferencesActivity" /> + <activity android:name=".StatusListActivity" /> + <activity android:name=".components.accountlist.AccountListActivity" /> + <activity android:name=".AboutActivity" /> + <activity android:name=".TabPreferenceActivity" /> + <activity + android:name="com.canhub.cropper.CropImageActivity" + android:theme="@style/Base.Theme.AppCompat" /> + <activity + android:name=".components.search.SearchActivity" + android:launchMode="singleTask" + android:exported="false"> + <intent-filter> + <action android:name="android.intent.action.SEARCH" /> + </intent-filter> + + <meta-data + android:name="android.app.searchable" + android:resource="@xml/searchable" /> + </activity> + <activity android:name=".ListsActivity" /> + <activity android:name=".LicenseActivity" /> + <activity android:name=".components.filters.FiltersActivity" /> + <activity android:name=".components.trending.TrendingActivity" /> + <activity android:name=".components.followedtags.FollowedTagsActivity" /> + <activity + android:name=".components.report.ReportActivity" + android:windowSoftInputMode="stateAlwaysHidden|adjustResize" /> + <activity android:name=".components.domainblocks.DomainBlocksActivity" /> + <activity android:name=".components.scheduled.ScheduledStatusActivity" /> + <activity android:name=".components.announcements.AnnouncementsActivity" /> + <activity android:name=".components.drafts.DraftsActivity" /> + <activity android:name="com.keylesspalace.tusky.components.filters.EditFilterActivity" + android:windowSoftInputMode="adjustResize" /> + + <receiver + android:name=".receiver.SendStatusBroadcastReceiver" + android:enabled="true" + android:exported="false" /> + <receiver + android:exported="true" + android:enabled="true" + android:name=".receiver.UnifiedPushBroadcastReceiver" + tools:ignore="ExportedReceiver"> + <intent-filter> + <action android:name="org.unifiedpush.android.connector.MESSAGE"/> + <action android:name="org.unifiedpush.android.connector.UNREGISTERED"/> + <action android:name="org.unifiedpush.android.connector.NEW_ENDPOINT"/> + <action android:name="org.unifiedpush.android.connector.REGISTRATION_FAILED"/> + <action android:name="org.unifiedpush.android.connector.REGISTRATION_REFUSED"/> + </intent-filter> + </receiver> + <receiver + android:exported="true" + android:enabled="true" + android:name=".receiver.NotificationBlockStateBroadcastReceiver" + tools:ignore="ExportedReceiver"> + <intent-filter> + <action android:name="android.app.action.NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED"/> + <action android:name="android.app.action.NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED"/> + </intent-filter> + </receiver> + + <service + android:name=".service.TuskyTileService" + android:icon="@drawable/ic_quicksettings" + android:label="@string/tusky_compose_post_quicksetting_label" + android:permission="android.permission.BIND_QUICK_SETTINGS_TILE" + android:exported="true"> + <intent-filter> + <action android:name="android.service.quicksettings.action.QS_TILE" /> + </intent-filter> + </service> + + <service android:name=".service.SendStatusService" + android:foregroundServiceType="shortService" + android:exported="false" /> + + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="${applicationId}.fileprovider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/file_paths" /> + </provider> + + <!-- disable automatic WorkManager initialization --> + <provider + android:name="androidx.startup.InitializationProvider" + android:authorities="${applicationId}.androidx-startup" + android:exported="false" + tools:node="merge"> + <meta-data + android:name="androidx.work.WorkManagerInitializer" + android:value="androidx.startup" + tools:node="remove" /> + </provider> + + </application> + +</manifest> diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 0000000..3132225 Binary files /dev/null and b/app/src/main/ic_launcher-web.png differ diff --git a/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt new file mode 100644 index 0000000..93cb084 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/AboutActivity.kt @@ -0,0 +1,121 @@ +package com.keylesspalace.tusky + +import android.content.Intent +import android.os.Build +import android.os.Bundle +import android.text.SpannableString +import android.text.SpannableStringBuilder +import android.text.method.LinkMovementMethod +import android.text.style.URLSpan +import android.text.util.Linkify +import android.widget.TextView +import androidx.annotation.StringRes +import androidx.lifecycle.lifecycleScope +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository +import com.keylesspalace.tusky.databinding.ActivityAboutBinding +import com.keylesspalace.tusky.util.NoUnderlineURLSpan +import com.keylesspalace.tusky.util.copyToClipboard +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class AboutActivity : BottomSheetActivity() { + @Inject + lateinit var instanceInfoRepository: InstanceInfoRepository + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val binding = ActivityAboutBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + setTitle(R.string.about_title_activity) + + binding.versionTextView.text = getString(R.string.about_app_version, getString(R.string.app_name), BuildConfig.VERSION_NAME) + + binding.deviceInfo.text = getString( + R.string.about_device_info, + Build.MANUFACTURER, + Build.MODEL, + Build.VERSION.RELEASE, + Build.VERSION.SDK_INT + ) + + lifecycleScope.launch { + accountManager.activeAccount?.let { account -> + val instanceInfo = instanceInfoRepository.getUpdatedInstanceInfoOrFallback() + binding.accountInfo.text = getString( + R.string.about_account_info, + account.username, + account.domain, + instanceInfo.version + ) + binding.accountInfoTitle.show() + binding.accountInfo.show() + } + } + + if (BuildConfig.CUSTOM_INSTANCE.isBlank()) { + binding.aboutPoweredByTusky.hide() + } + + binding.aboutLicenseInfoTextView.setClickableTextWithoutUnderlines( + R.string.about_tusky_license + ) + binding.aboutWebsiteInfoTextView.setClickableTextWithoutUnderlines( + R.string.about_project_site + ) + binding.aboutBugsFeaturesInfoTextView.setClickableTextWithoutUnderlines( + R.string.about_bug_feature_request_site + ) + + binding.tuskyProfileButton.setOnClickListener { + viewUrl(BuildConfig.SUPPORT_ACCOUNT_URL) + } + + binding.aboutLicensesButton.setOnClickListener { + startActivityWithSlideInAnimation(Intent(this, LicenseActivity::class.java)) + } + + binding.copyDeviceInfo.setOnClickListener { + copyToClipboard( + "${binding.versionTextView.text}\n\nDevice:\n\n${binding.deviceInfo.text}\n\nAccount:\n\n${binding.accountInfo.text}", + getString(R.string.about_copied), + "Tusky version information", + ) + } + } +} + +private fun TextView.setClickableTextWithoutUnderlines(@StringRes textId: Int) { + val text = SpannableString(context.getText(textId)) + + Linkify.addLinks(text, Linkify.WEB_URLS) + + val builder = SpannableStringBuilder(text) + val urlSpans = text.getSpans(0, text.length, URLSpan::class.java) + for (span in urlSpans) { + val start = builder.getSpanStart(span) + val end = builder.getSpanEnd(span) + val flags = builder.getSpanFlags(span) + + val customSpan = NoUnderlineURLSpan(span.url) + + builder.removeSpan(span) + builder.setSpan(customSpan, start, end, flags) + } + + setText(builder) + linksClickable = true + movementMethod = LinkMovementMethod.getInstance() +} diff --git a/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt new file mode 100644 index 0000000..3b5d851 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/AccountsInListFragment.kt @@ -0,0 +1,290 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import androidx.appcompat.widget.SearchView +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.ListAdapter +import com.keylesspalace.tusky.databinding.FragmentAccountsInListBinding +import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.unsafeLazy +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.viewmodel.AccountsInListViewModel +import com.keylesspalace.tusky.viewmodel.State +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.launch + +private typealias AccountInfo = Pair<TimelineAccount, Boolean> + +@AndroidEntryPoint +class AccountsInListFragment : DialogFragment(R.layout.fragment_accounts_in_list) { + + @Inject + lateinit var preferences: SharedPreferences + + private val viewModel: AccountsInListViewModel by viewModels() + private val binding by viewBinding(FragmentAccountsInListBinding::bind) + + private lateinit var listId: String + private lateinit var listName: String + + private val radius by unsafeLazy { resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle) + val args = requireArguments() + listId = args.getString(LIST_ID_ARG)!! + listName = args.getString(LIST_NAME_ARG)!! + + viewModel.load(listId) + } + + override fun onStart() { + super.onStart() + dialog?.apply { + // Stretch dialog to the window + window?.setLayout( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT + ) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val adapter = Adapter() + val searchAdapter = SearchAdapter() + + binding.accountsRecycler.layoutManager = LinearLayoutManager(view.context) + binding.accountsRecycler.adapter = adapter + + binding.accountsSearchRecycler.layoutManager = LinearLayoutManager(view.context) + binding.accountsSearchRecycler.adapter = searchAdapter + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.state.collect { state -> + adapter.submitList(state.accounts.getOrDefault(emptyList())) + + state.accounts.fold( + onSuccess = { binding.messageView.hide() }, + onFailure = { handleError(it) } + ) + + setupSearchView(searchAdapter, state) + } + } + + binding.searchView.isSubmitButtonEnabled = true + binding.searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { + override fun onQueryTextSubmit(query: String?): Boolean { + viewModel.search(query.orEmpty()) + return true + } + + override fun onQueryTextChange(newText: String?): Boolean { + // Close event is not sent so we use this instead + if (newText.isNullOrBlank()) { + viewModel.search("") + } + return true + } + }) + } + + private fun setupSearchView(searchAdapter: SearchAdapter, state: State) { + if (state.searchResult == null) { + searchAdapter.submitList(listOf()) + binding.accountsSearchRecycler.hide() + binding.accountsRecycler.show() + } else { + val listAccounts = state.accounts.getOrDefault(emptyList()) + val newList = state.searchResult.map { acc -> + acc to listAccounts.contains(acc) + } + searchAdapter.submitList(newList) + binding.accountsSearchRecycler.show() + binding.accountsRecycler.hide() + } + } + + private fun handleError(error: Throwable) { + binding.messageView.show() + binding.messageView.setup(error) { _: View -> + binding.messageView.hide() + viewModel.load(listId) + } + } + + private fun onRemoveFromList(accountId: String) { + viewModel.deleteAccountFromList(listId, accountId) + } + + private fun onAddToList(account: TimelineAccount) { + viewModel.addAccountToList(listId, account) + } + + private object AccountDiffer : DiffUtil.ItemCallback<TimelineAccount>() { + override fun areItemsTheSame(oldItem: TimelineAccount, newItem: TimelineAccount): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: TimelineAccount, + newItem: TimelineAccount + ): Boolean { + return oldItem == newItem + } + } + + inner class Adapter : ListAdapter<TimelineAccount, BindingHolder<ItemFollowRequestBinding>>( + AccountDiffer + ) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemFollowRequestBinding> { + val binding = ItemFollowRequestBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + val holder = BindingHolder(binding) + + binding.notificationTextView.hide() + binding.acceptButton.hide() + binding.rejectButton.setOnClickListener { + onRemoveFromList(getItem(holder.bindingAdapterPosition).id) + } + binding.rejectButton.contentDescription = + binding.root.context.getString(R.string.action_remove_from_list) + + return holder + } + + override fun onBindViewHolder( + holder: BindingHolder<ItemFollowRequestBinding>, + position: Int + ) { + val account = getItem(position) + val animateAvatar = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) + val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + val showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, false) + holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis) + holder.binding.usernameTextView.text = account.username + holder.binding.avatarBadge.visible(showBotOverlay && account.bot) + loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar) + } + } + + private object SearchDiffer : DiffUtil.ItemCallback<AccountInfo>() { + override fun areItemsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean { + return oldItem.first.id == newItem.first.id + } + + override fun areContentsTheSame(oldItem: AccountInfo, newItem: AccountInfo): Boolean { + return oldItem == newItem + } + } + + inner class SearchAdapter : ListAdapter<AccountInfo, BindingHolder<ItemFollowRequestBinding>>( + SearchDiffer + ) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemFollowRequestBinding> { + val binding = ItemFollowRequestBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + val holder = BindingHolder(binding) + + binding.notificationTextView.hide() + binding.acceptButton.hide() + binding.rejectButton.setOnClickListener { + val (account, inAList) = getItem(holder.bindingAdapterPosition) + if (inAList) { + onRemoveFromList(account.id) + } else { + onAddToList(account) + } + } + + return holder + } + + override fun onBindViewHolder( + holder: BindingHolder<ItemFollowRequestBinding>, + position: Int + ) { + val (account, inAList) = getItem(position) + + val animateAvatar = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) + val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + + holder.binding.displayNameTextView.text = account.name.emojify(account.emojis, holder.binding.displayNameTextView, animateEmojis) + holder.binding.usernameTextView.text = account.username + loadAvatar(account.avatar, holder.binding.avatar, radius, animateAvatar) + + holder.binding.rejectButton.apply { + contentDescription = if (inAList) { + setImageResource(R.drawable.ic_reject_24dp) + getString(R.string.action_remove_from_list) + } else { + setImageResource(R.drawable.ic_plus_24dp) + getString(R.string.action_add_to_list) + } + } + } + } + + companion object { + private const val LIST_ID_ARG = "listId" + private const val LIST_NAME_ARG = "listName" + + @JvmStatic + fun newInstance(listId: String, listName: String): AccountsInListFragment { + val args = Bundle().apply { + putString(LIST_ID_ARG, listId) + putString(LIST_NAME_ARG, listName) + } + return AccountsInListFragment().apply { arguments = args } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java new file mode 100644 index 0000000..2bbcf28 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/BaseActivity.java @@ -0,0 +1,294 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky; + +import android.app.ActivityManager; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Color; +import android.os.Bundle; +import android.util.Log; +import android.view.MenuItem; +import android.view.View; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.ViewModelProvider; + +import com.google.android.material.color.MaterialColors; +import com.google.android.material.snackbar.Snackbar; +import com.keylesspalace.tusky.adapter.AccountSelectionAdapter; +import com.keylesspalace.tusky.components.login.LoginActivity; +import com.keylesspalace.tusky.db.entity.AccountEntity; +import com.keylesspalace.tusky.db.AccountManager; +import com.keylesspalace.tusky.di.PreferencesEntryPoint; +import com.keylesspalace.tusky.interfaces.AccountSelectionListener; +import com.keylesspalace.tusky.settings.AppTheme; +import com.keylesspalace.tusky.settings.PrefKeys; +import com.keylesspalace.tusky.util.ActivityConstants; +import com.keylesspalace.tusky.util.ActivityExtensions; +import com.keylesspalace.tusky.util.ThemeUtils; + +import java.util.List; + +import javax.inject.Inject; + +import static com.keylesspalace.tusky.settings.PrefKeys.APP_THEME; + +import dagger.hilt.EntryPoints; + +/** + * All activities inheriting from BaseActivity must be annotated with @AndroidEntryPoint + */ +public abstract class BaseActivity extends AppCompatActivity { + + public static final String OPEN_WITH_SLIDE_IN = "OPEN_WITH_SLIDE_IN"; + + private static final String TAG = "BaseActivity"; + + @Inject + @NonNull + public AccountManager accountManager; + + @Inject + @NonNull + public SharedPreferences preferences; + + /** + * Allows overriding the default ViewModelProvider.Factory for testing purposes. + */ + @Nullable + public ViewModelProvider.Factory viewModelProviderFactory = null; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (activityTransitionWasRequested()) { + ActivityExtensions.overrideActivityTransitionCompat( + this, + ActivityConstants.OVERRIDE_TRANSITION_OPEN, + R.anim.activity_open_enter, + R.anim.activity_open_exit + ); + ActivityExtensions.overrideActivityTransitionCompat( + this, + ActivityConstants.OVERRIDE_TRANSITION_CLOSE, + R.anim.activity_close_enter, + R.anim.activity_close_exit + ); + } + + /* There isn't presently a way to globally change the theme of a whole application at + * runtime, just individual activities. So, each activity has to set its theme before any + * views are created. */ + String theme = preferences.getString(APP_THEME, AppTheme.DEFAULT.getValue()); + Log.d("activeTheme", theme); + if (ThemeUtils.isBlack(getResources().getConfiguration(), theme)) { + setTheme(R.style.TuskyBlackTheme); + } else if (this instanceof MainActivity) { + // Replace the SplashTheme of MainActivity + setTheme(R.style.TuskyTheme); + } + + /* set the taskdescription programmatically, the theme would turn it blue */ + String appName = getString(R.string.app_name); + Bitmap appIcon = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher); + int recentsBackgroundColor = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurface, Color.BLACK); + + setTaskDescription(new ActivityManager.TaskDescription(appName, appIcon, recentsBackgroundColor)); + + int style = textStyle(preferences.getString(PrefKeys.STATUS_TEXT_SIZE, "medium")); + getTheme().applyStyle(style, true); + + if(requiresLogin()) { + redirectIfNotLoggedIn(); + } + } + + private boolean activityTransitionWasRequested() { + return getIntent().getBooleanExtra(OPEN_WITH_SLIDE_IN, false); + } + + @Override + protected void attachBaseContext(Context newBase) { + // injected preferences not yet available at this point of the lifecycle + SharedPreferences preferences = EntryPoints.get(newBase.getApplicationContext(), PreferencesEntryPoint.class).preferences(); + + // Scale text in the UI from PrefKeys.UI_TEXT_SCALE_RATIO + float uiScaleRatio = preferences.getFloat(PrefKeys.UI_TEXT_SCALE_RATIO, 100F); + + Configuration configuration = newBase.getResources().getConfiguration(); + + // Adjust `fontScale` in the configuration. + // + // You can't repeatedly adjust the `fontScale` in `newBase` because that will contain the + // result of previous adjustments. E.g., going from 100% to 80% to 100% does not return + // you to the original 100%, it leaves it at 80%. + // + // Instead, calculate the new scale from the application context. This is unaffected by + // changes to the base context. It does contain contain any changes to the font scale from + // "Settings > Display > Font size" in the device settings, so scaling performed here + // is in addition to any scaling in the device settings. + Configuration appConfiguration = newBase.getApplicationContext().getResources().getConfiguration(); + + // This only adjusts the fonts, anything measured in `dp` is unaffected by this. + // You can try to adjust `densityDpi` as shown in the commented out code below. This + // works, to a point. However, dialogs do not react well to this. Beyond a certain + // scale (~ 120%) the right hand edge of the dialog will clip off the right of the + // screen. + // + // So for now, just adjust the font scale + // + // val displayMetrics = appContext.resources.displayMetrics + // configuration.densityDpi = ((displayMetrics.densityDpi * uiScaleRatio).toInt()) + configuration.fontScale = appConfiguration.fontScale * uiScaleRatio / 100F; + + Context fontScaleContext = newBase.createConfigurationContext(configuration); + + super.attachBaseContext(fontScaleContext); + } + + @NonNull + @Override + public ViewModelProvider.Factory getDefaultViewModelProviderFactory() { + final ViewModelProvider.Factory factory = viewModelProviderFactory; + return (factory != null) ? factory : super.getDefaultViewModelProviderFactory(); + } + + protected boolean requiresLogin() { + return true; + } + + private static int textStyle(String name) { + int style; + switch (name) { + case "smallest": + style = R.style.TextSizeSmallest; + break; + case "small": + style = R.style.TextSizeSmall; + break; + case "medium": + default: + style = R.style.TextSizeMedium; + break; + case "large": + style = R.style.TextSizeLarge; + break; + case "largest": + style = R.style.TextSizeLargest; + break; + } + return style; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + if (item.getItemId() == android.R.id.home) { + getOnBackPressedDispatcher().onBackPressed(); + return true; + } + return super.onOptionsItemSelected(item); + } + + protected void redirectIfNotLoggedIn() { + AccountEntity account = accountManager.getActiveAccount(); + if (account == null) { + Intent intent = LoginActivity.getIntent(this, LoginActivity.MODE_DEFAULT); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(intent); + finish(); + } + } + + protected void showErrorDialog(@Nullable View anyView, @StringRes int descriptionId, @StringRes int actionId, @Nullable View.OnClickListener listener) { + if (anyView != null) { + Snackbar bar = Snackbar.make(anyView, getString(descriptionId), Snackbar.LENGTH_SHORT); + bar.setAction(actionId, listener); + bar.show(); + } + } + + public void showAccountChooserDialog(@Nullable CharSequence dialogTitle, boolean showActiveAccount, @NonNull AccountSelectionListener listener) { + List<AccountEntity> accounts = accountManager.getAllAccountsOrderedByActive(); + AccountEntity activeAccount = accountManager.getActiveAccount(); + + switch(accounts.size()) { + case 1: + listener.onAccountSelected(activeAccount); + return; + case 2: + if (!showActiveAccount) { + for (AccountEntity account : accounts) { + if (activeAccount != account) { + listener.onAccountSelected(account); + return; + } + } + } + break; + } + + if (!showActiveAccount && activeAccount != null) { + accounts.remove(activeAccount); + } + AccountSelectionAdapter adapter = new AccountSelectionAdapter( + this, + preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + ); + adapter.addAll(accounts); + + new AlertDialog.Builder(this) + .setTitle(dialogTitle) + .setAdapter(adapter, (dialogInterface, index) -> listener.onAccountSelected(accounts.get(index))) + .show(); + } + + public @Nullable String getOpenAsText() { + List<AccountEntity> accounts = accountManager.getAllAccountsOrderedByActive(); + switch (accounts.size()) { + case 0: + case 1: + return null; + case 2: + for (AccountEntity account : accounts) { + if (account != accountManager.getActiveAccount()) { + return String.format(getString(R.string.action_open_as), account.getFullName()); + } + } + return null; + default: + return String.format(getString(R.string.action_open_as), "…"); + } + } + + public void openAsAccount(@NonNull String url, @NonNull AccountEntity account) { + accountManager.setActiveAccount(account.getId()); + Intent intent = MainActivity.redirectIntent(this, account.getId(), url); + + startActivity(intent); + finish(); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt new file mode 100644 index 0000000..73c87cf --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/BottomSheetActivity.kt @@ -0,0 +1,191 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.View +import android.widget.LinearLayout +import android.widget.Toast +import androidx.annotation.VisibleForTesting +import androidx.lifecycle.lifecycleScope +import at.connyduck.calladapter.networkresult.fold +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.keylesspalace.tusky.components.account.AccountActivity +import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.looksLikeMastodonUrl +import com.keylesspalace.tusky.util.openLink +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import javax.inject.Inject +import kotlinx.coroutines.launch + +/** this is the base class for all activities that open links + * links are checked against the api if they are mastodon links so they can be opened in Tusky + * Subclasses must have a bottom sheet with Id item_status_bottom_sheet in their layout hierarchy + */ + +abstract class BottomSheetActivity : BaseActivity() { + + lateinit var bottomSheet: BottomSheetBehavior<LinearLayout> + var searchUrl: String? = null + + @Inject + lateinit var mastodonApi: MastodonApi + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + + val bottomSheetLayout: LinearLayout = findViewById(R.id.item_status_bottom_sheet) + bottomSheet = BottomSheetBehavior.from(bottomSheetLayout) + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + bottomSheet.addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + if (newState == BottomSheetBehavior.STATE_HIDDEN) { + cancelActiveSearch() + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) {} + }) + } + + open fun viewUrl( + url: String, + lookupFallbackBehavior: PostLookupFallbackBehavior = PostLookupFallbackBehavior.OPEN_IN_BROWSER + ) { + if (!looksLikeMastodonUrl(url)) { + openLink(url) + return + } + + lifecycleScope.launch { + mastodonApi.search( + query = url, + resolve = true + ).fold( + onSuccess = { (accounts, statuses) -> + if (getCancelSearchRequested(url)) { + return@launch + } + + onEndSearch(url) + + if (statuses.isNotEmpty()) { + viewThread(statuses[0].id, statuses[0].url) + return@launch + } + accounts.firstOrNull { it.url.equals(url, ignoreCase = true) }?.let { account -> + // Some servers return (unrelated) accounts for url searches (#2804) + // Verify that the account's url matches the query + viewAccount(account.id) + return@launch + } + + performUrlFallbackAction(url, lookupFallbackBehavior) + }, + onFailure = { + if (!getCancelSearchRequested(url)) { + onEndSearch(url) + performUrlFallbackAction(url, lookupFallbackBehavior) + } + } + ) + } + + onBeginSearch(url) + } + + open fun viewThread(statusId: String, url: String?) { + if (!isSearching()) { + val intent = Intent(this, ViewThreadActivity::class.java) + intent.putExtra("id", statusId) + intent.putExtra("url", url) + startActivityWithSlideInAnimation(intent) + } + } + + open fun viewAccount(id: String) { + val intent = AccountActivity.getIntent(this, id) + startActivityWithSlideInAnimation(intent) + } + + protected open fun performUrlFallbackAction( + url: String, + fallbackBehavior: PostLookupFallbackBehavior + ) { + when (fallbackBehavior) { + PostLookupFallbackBehavior.OPEN_IN_BROWSER -> openLink(url) + PostLookupFallbackBehavior.DISPLAY_ERROR -> Toast.makeText( + this, + getString(R.string.post_lookup_error_format, url), + Toast.LENGTH_SHORT + ).show() + } + } + + @VisibleForTesting + fun onBeginSearch(url: String) { + searchUrl = url + showQuerySheet() + } + + @VisibleForTesting + fun getCancelSearchRequested(url: String): Boolean { + return url != searchUrl + } + + @VisibleForTesting + fun isSearching(): Boolean { + return searchUrl != null + } + + @VisibleForTesting + fun onEndSearch(url: String?) { + if (url == searchUrl) { + // Don't clear query if there's no match, + // since we might just now be getting the response for a canceled search + searchUrl = null + hideQuerySheet() + } + } + + @VisibleForTesting + fun cancelActiveSearch() { + if (isSearching()) { + onEndSearch(searchUrl) + } + } + + @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) + open fun openLink(url: String) { + (this as Context).openLink(url) + } + + private fun showQuerySheet() { + bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED + } + + private fun hideQuerySheet() { + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + } +} + +enum class PostLookupFallbackBehavior { + OPEN_IN_BROWSER, + DISPLAY_ERROR +} diff --git a/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt new file mode 100644 index 0000000..ab459ae --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/EditProfileActivity.kt @@ -0,0 +1,372 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky + +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Color +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.ImageView +import androidx.activity.OnBackPressedCallback +import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog +import androidx.core.view.isVisible +import androidx.core.widget.doAfterTextChanged +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.bumptech.glide.load.resource.bitmap.FitCenter +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.canhub.cropper.CropImage +import com.canhub.cropper.CropImageContract +import com.canhub.cropper.options +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository +import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding +import com.keylesspalace.tusky.util.Error +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.await +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.viewmodel.EditProfileViewModel +import com.keylesspalace.tusky.viewmodel.ProfileDataInUi +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class EditProfileActivity : BaseActivity() { + + companion object { + const val AVATAR_SIZE = 400 + const val HEADER_WIDTH = 1500 + const val HEADER_HEIGHT = 500 + } + + private val viewModel: EditProfileViewModel by viewModels() + + private val binding by viewBinding(ActivityEditProfileBinding::inflate) + + private val accountFieldEditAdapter = AccountFieldEditAdapter() + + private var maxAccountFields = InstanceInfoRepository.DEFAULT_MAX_ACCOUNT_FIELDS + + private enum class PickType { + AVATAR, + HEADER + } + + private val cropImage = registerForActivityResult(CropImageContract()) { result -> + if (result is CropImage.CancelledResult) { + return@registerForActivityResult + } + + if (!result.isSuccessful) { + return@registerForActivityResult onPickFailure(result.error) + } + + if (result.uriContent == viewModel.getAvatarUri()) { + viewModel.newAvatarPicked() + } else { + viewModel.newHeaderPicked() + } + } + + private val currentProfileData + get() = ProfileDataInUi( + displayName = binding.displayNameEditText.text.toString(), + note = binding.noteEditText.text.toString(), + locked = binding.lockedCheckBox.isChecked, + fields = accountFieldEditAdapter.getFieldData() + ) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.run { + setTitle(R.string.title_edit_profile) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + binding.avatarButton.setOnClickListener { pickMedia(PickType.AVATAR) } + binding.headerButton.setOnClickListener { pickMedia(PickType.HEADER) } + + binding.fieldList.layoutManager = LinearLayoutManager(this) + binding.fieldList.adapter = accountFieldEditAdapter + + val plusDrawable = IconicsDrawable(this, GoogleMaterial.Icon.gmd_add).apply { + sizeDp = 12 + colorInt = Color.WHITE + } + + binding.addFieldButton.setCompoundDrawablesRelativeWithIntrinsicBounds( + plusDrawable, + null, + null, + null + ) + + binding.addFieldButton.setOnClickListener { + accountFieldEditAdapter.addField() + if (accountFieldEditAdapter.itemCount >= maxAccountFields) { + it.isVisible = false + } + + binding.scrollView.post { + binding.scrollView.smoothScrollTo(0, it.bottom) + } + } + + viewModel.obtainProfile() + + lifecycleScope.launch { + viewModel.profileData.collect { profileRes -> + if (profileRes == null) return@collect + when (profileRes) { + is Success -> { + val me = profileRes.data + if (me != null) { + binding.displayNameEditText.setText(me.displayName) + binding.noteEditText.setText(me.source?.note) + binding.lockedCheckBox.isChecked = me.locked + + accountFieldEditAdapter.setFields(me.source?.fields.orEmpty()) + binding.addFieldButton.isVisible = + (me.source?.fields?.size ?: 0) < maxAccountFields + + if (viewModel.avatarData.value == null) { + Glide.with(this@EditProfileActivity) + .load(me.avatar) + .placeholder(R.drawable.avatar_default) + .transform( + FitCenter(), + RoundedCorners( + resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp) + ) + ) + .into(binding.avatarPreview) + } + + if (viewModel.headerData.value == null) { + Glide.with(this@EditProfileActivity) + .load(me.header) + .into(binding.headerPreview) + } + } + } + is Error -> { + Snackbar.make( + binding.avatarButton, + R.string.error_generic, + Snackbar.LENGTH_LONG + ) + .setAction(R.string.action_retry) { + viewModel.obtainProfile() + } + .show() + } + is Loading -> { } + } + } + } + + lifecycleScope.launch { + viewModel.instanceData.collect { instanceInfo -> + maxAccountFields = instanceInfo.maxFields + accountFieldEditAdapter.setFieldLimits( + instanceInfo.maxFieldNameLength, + instanceInfo.maxFieldValueLength + ) + binding.addFieldButton.isVisible = + accountFieldEditAdapter.itemCount < maxAccountFields + } + } + + observeImage(viewModel.avatarData, binding.avatarPreview, true) + observeImage(viewModel.headerData, binding.headerPreview, false) + + lifecycleScope.launch { + viewModel.saveData.collect { + if (it == null) return@collect + when (it) { + is Success -> { + finish() + } + is Loading -> { + binding.saveProgressBar.visibility = View.VISIBLE + } + is Error -> { + onSaveFailure(it.errorMessage) + } + } + } + } + + binding.displayNameEditText.doAfterTextChanged { + viewModel.dataChanged(currentProfileData) + } + + binding.displayNameEditText.doAfterTextChanged { + viewModel.dataChanged(currentProfileData) + } + + binding.lockedCheckBox.setOnCheckedChangeListener { _, _ -> + viewModel.dataChanged(currentProfileData) + } + + accountFieldEditAdapter.onFieldsChanged = { + viewModel.dataChanged(currentProfileData) + } + + val onBackCallback = object : OnBackPressedCallback(enabled = false) { + override fun handleOnBackPressed() { + showUnsavedChangesDialog() + } + } + + onBackPressedDispatcher.addCallback(this, onBackCallback) + lifecycleScope.launch { + viewModel.isChanged.collect { dataWasChanged -> + onBackCallback.isEnabled = dataWasChanged + } + } + } + + override fun onStop() { + super.onStop() + if (!isFinishing) { + viewModel.updateProfile(currentProfileData) + } + } + + private fun observeImage( + flow: StateFlow<Uri?>, + imageView: ImageView, + roundedCorners: Boolean + ) { + lifecycleScope.launch { + flow.collect { imageUri -> + + // skipping all caches so we can always reuse the same uri + val glide = Glide.with(imageView) + .load(imageUri) + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + + if (roundedCorners) { + glide.transform( + FitCenter(), + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp)) + ).into(imageView) + } else { + glide.into(imageView) + } + + imageView.show() + } + } + } + + private fun pickMedia(pickType: PickType) { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "image/*" + when (pickType) { + PickType.AVATAR -> { + cropImage.launch( + options { + setRequestedSize(AVATAR_SIZE, AVATAR_SIZE) + setAspectRatio(AVATAR_SIZE, AVATAR_SIZE) + setImageSource(includeGallery = true, includeCamera = false) + setOutputUri(viewModel.getAvatarUri()) + setOutputCompressFormat(Bitmap.CompressFormat.PNG) + } + ) + } + PickType.HEADER -> { + cropImage.launch( + options { + setRequestedSize(HEADER_WIDTH, HEADER_HEIGHT) + setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT) + setImageSource(includeGallery = true, includeCamera = false) + setOutputUri(viewModel.getHeaderUri()) + setOutputCompressFormat(Bitmap.CompressFormat.PNG) + } + ) + } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.edit_profile_toolbar, menu) + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_save -> { + save() + return true + } + } + return super.onOptionsItemSelected(item) + } + + private fun save() = viewModel.save(currentProfileData) + + private fun onSaveFailure(msg: String?) { + val errorMsg = msg ?: getString(R.string.error_media_upload_sending) + Snackbar.make(binding.avatarButton, errorMsg, Snackbar.LENGTH_LONG).show() + binding.saveProgressBar.visibility = View.GONE + } + + private fun onPickFailure(throwable: Throwable?) { + Log.w("EditProfileActivity", "failed to pick media", throwable) + Snackbar.make( + binding.avatarButton, + R.string.error_media_upload_sending, + Snackbar.LENGTH_LONG + ).show() + } + + private fun showUnsavedChangesDialog() = lifecycleScope.launch { + when (launchSaveDialog()) { + AlertDialog.BUTTON_POSITIVE -> save() + else -> finish() + } + } + + private suspend fun launchSaveDialog() = AlertDialog.Builder(this) + .setMessage(getString(R.string.dialog_save_profile_changes_message)) + .create() + .await(R.string.action_save, R.string.action_discard) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt b/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt new file mode 100644 index 0000000..426fa1c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/LicenseActivity.kt @@ -0,0 +1,63 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky + +import android.os.Bundle +import android.util.Log +import android.widget.TextView +import androidx.annotation.RawRes +import androidx.lifecycle.lifecycleScope +import com.keylesspalace.tusky.databinding.ActivityLicenseBinding +import dagger.hilt.android.AndroidEntryPoint +import java.io.IOException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okio.buffer +import okio.source + +@AndroidEntryPoint +class LicenseActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val binding = ActivityLicenseBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + setTitle(R.string.title_licenses) + + loadFileIntoTextView(R.raw.apache, binding.licenseApacheTextView) + } + + private fun loadFileIntoTextView(@RawRes fileId: Int, textView: TextView) { + lifecycleScope.launch { + textView.text = withContext(Dispatchers.IO) { + try { + resources.openRawResource(fileId).source().buffer().use { it.readUtf8() } + } catch (e: IOException) { + Log.w("LicenseActivity", e) + "" + } + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt new file mode 100644 index 0000000..0323c29 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ListsActivity.kt @@ -0,0 +1,283 @@ +/* Copyright Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky + +import android.app.Dialog +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.PopupMenu +import androidx.activity.viewModels +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import androidx.core.widget.doOnTextChanged +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.ListAdapter +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.databinding.ActivityListsBinding +import com.keylesspalace.tusky.databinding.DialogListBinding +import com.keylesspalace.tusky.databinding.ItemListBinding +import com.keylesspalace.tusky.entity.MastoList +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.viewmodel.ListsViewModel +import com.keylesspalace.tusky.viewmodel.ListsViewModel.Event +import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_NETWORK +import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.ERROR_OTHER +import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.INITIAL +import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADED +import com.keylesspalace.tusky.viewmodel.ListsViewModel.LoadingState.LOADING +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +// TODO use the ListSelectionFragment (and/or its adapter or binding) here; but keep the LoadingState from here (?) + +@AndroidEntryPoint +class ListsActivity : BaseActivity() { + + private val viewModel: ListsViewModel by viewModels() + + private val binding by viewBinding(ActivityListsBinding::inflate) + + private val adapter = ListsAdapter() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.apply { + title = getString(R.string.title_lists) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + binding.listsRecycler.adapter = adapter + binding.listsRecycler.layoutManager = LinearLayoutManager(this) + binding.listsRecycler.addItemDecoration( + DividerItemDecoration(this, DividerItemDecoration.VERTICAL) + ) + + binding.swipeRefreshLayout.setOnRefreshListener { viewModel.retryLoading() } + + lifecycleScope.launch { + viewModel.state.collect(this@ListsActivity::update) + } + + viewModel.retryLoading() + + binding.addListButton.setOnClickListener { + showlistNameDialog(null) + } + + lifecycleScope.launch { + viewModel.events.collect { event -> + when (event) { + Event.CREATE_ERROR -> showMessage(R.string.error_create_list) + Event.UPDATE_ERROR -> showMessage(R.string.error_rename_list) + Event.DELETE_ERROR -> showMessage(R.string.error_delete_list) + } + } + } + } + + private fun showlistNameDialog(list: MastoList?) { + val binding = DialogListBinding.inflate(layoutInflater).apply { + replyPolicySpinner.setSelection(MastoList.ReplyPolicy.from(list?.repliesPolicy).ordinal) + } + val dialog = AlertDialog.Builder(this) + .setView(binding.root) + .setPositiveButton( + if (list == null) { + R.string.action_create_list + } else { + R.string.action_rename_list + } + ) { _, _ -> + onPickedDialogName( + binding.nameText.text.toString(), + list?.id, + binding.exclusiveCheckbox.isChecked, + MastoList.ReplyPolicy.entries[binding.replyPolicySpinner.selectedItemPosition].policy + ) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + + binding.nameText.let { editText -> + editText.doOnTextChanged { s, _, _, _ -> + dialog.getButton(Dialog.BUTTON_POSITIVE).isEnabled = s?.isNotBlank() == true + } + editText.setText(list?.title) + editText.text?.let { editText.setSelection(it.length) } + } + + list?.let { + if (it.exclusive == null) { + binding.exclusiveCheckbox.visible(false) + } else { + binding.exclusiveCheckbox.isChecked = it.exclusive + } + } + } + + private fun showListDeleteDialog(list: MastoList) { + AlertDialog.Builder(this) + .setMessage(getString(R.string.dialog_delete_list_warning, list.title)) + .setPositiveButton(R.string.action_delete) { _, _ -> + viewModel.deleteList(list.id) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun update(state: ListsViewModel.State) { + adapter.submitList(state.lists) + binding.progressBar.visible(state.loadingState == LOADING) + binding.swipeRefreshLayout.isRefreshing = state.loadingState == LOADING + when (state.loadingState) { + INITIAL, LOADING -> binding.messageView.hide() + ERROR_NETWORK -> { + binding.messageView.show() + binding.messageView.setup(R.drawable.errorphant_offline, R.string.error_network) { + viewModel.retryLoading() + } + } + ERROR_OTHER -> { + binding.messageView.show() + binding.messageView.setup(R.drawable.errorphant_error, R.string.error_generic) { + viewModel.retryLoading() + } + } + LOADED -> + if (state.lists.isEmpty()) { + binding.messageView.show() + binding.messageView.setup( + R.drawable.elephant_friend_empty, + R.string.message_empty, + null + ) + binding.messageView.showHelp(R.string.help_empty_lists) + } else { + binding.messageView.hide() + } + } + } + + private fun showMessage(@StringRes messageId: Int) { + Snackbar.make( + binding.listsRecycler, + messageId, + Snackbar.LENGTH_SHORT + ).show() + } + + private fun onListSelected(list: MastoList) { + startActivityWithSlideInAnimation( + StatusListActivity.newListIntent(this, list.id, list.title) + ) + } + + private fun openListSettings(list: MastoList) { + AccountsInListFragment.newInstance(list.id, list.title).show(supportFragmentManager, null) + } + + private fun renameListDialog(list: MastoList) { + showlistNameDialog(list) + } + + private fun onMore(list: MastoList, view: View) { + PopupMenu(view.context, view).apply { + inflate(R.menu.list_actions) + setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.list_edit -> openListSettings(list) + R.id.list_update -> renameListDialog(list) + R.id.list_delete -> showListDeleteDialog(list) + else -> return@setOnMenuItemClickListener false + } + true + } + show() + } + } + + private object ListsDiffer : DiffUtil.ItemCallback<MastoList>() { + override fun areItemsTheSame(oldItem: MastoList, newItem: MastoList): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: MastoList, newItem: MastoList): Boolean { + return oldItem == newItem + } + } + + private inner class ListsAdapter : + ListAdapter<MastoList, BindingHolder<ItemListBinding>>(ListsDiffer) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemListBinding> { + return BindingHolder(ItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + } + + override fun onBindViewHolder(holder: BindingHolder<ItemListBinding>, position: Int) { + val item = getItem(position) + holder.binding.listName.text = item.title + + holder.binding.moreButton.apply { + visible(true) + setOnClickListener { + onMore(item, holder.binding.moreButton) + } + } + + holder.itemView.setOnClickListener { + onListSelected(item) + } + } + } + + private fun onPickedDialogName( + name: String, + listId: String?, + exclusive: Boolean, + replyPolicy: String + ) { + if (listId == null) { + viewModel.createNewList(name, exclusive, replyPolicy) + } else { + viewModel.updateList(listId, name, exclusive, replyPolicy) + } + } + + companion object { + fun newIntent(context: Context) = Intent(context, ListsActivity::class.java) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt new file mode 100644 index 0000000..c572b9f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/MainActivity.kt @@ -0,0 +1,1308 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky + +import android.Manifest +import android.annotation.SuppressLint +import android.app.NotificationManager +import android.content.Context +import android.content.DialogInterface +import android.content.Intent +import android.content.pm.PackageManager +import android.graphics.Bitmap +import android.graphics.Color +import android.graphics.drawable.Animatable +import android.graphics.drawable.BitmapDrawable +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.text.TextUtils +import android.util.Log +import android.view.KeyEvent +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.MenuItem.SHOW_AS_ACTION_NEVER +import android.view.View +import android.widget.ImageView +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.content.res.AppCompatResources +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.core.view.MenuProvider +import androidx.core.view.forEach +import androidx.core.view.isVisible +import androidx.lifecycle.lifecycleScope +import androidx.viewpager2.widget.MarginPageTransformer +import at.connyduck.calladapter.networkresult.fold +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.target.FixedSizeDrawable +import com.bumptech.glide.request.transition.Transition +import com.google.android.material.R as materialR +import com.google.android.material.color.MaterialColors +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayout.OnTabSelectedListener +import com.google.android.material.tabs.TabLayoutMediator +import com.keylesspalace.tusky.appstore.AnnouncementReadEvent +import com.keylesspalace.tusky.appstore.CacheUpdater +import com.keylesspalace.tusky.appstore.ConversationsLoadingEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.MainTabsChangedEvent +import com.keylesspalace.tusky.appstore.NewNotificationsEvent +import com.keylesspalace.tusky.appstore.NotificationsLoadingEvent +import com.keylesspalace.tusky.appstore.ProfileEditedEvent +import com.keylesspalace.tusky.components.account.AccountActivity +import com.keylesspalace.tusky.components.accountlist.AccountListActivity +import com.keylesspalace.tusky.components.announcements.AnnouncementsActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.canHandleMimeType +import com.keylesspalace.tusky.components.drafts.DraftsActivity +import com.keylesspalace.tusky.components.login.LoginActivity +import com.keylesspalace.tusky.components.preference.PreferencesActivity +import com.keylesspalace.tusky.components.scheduled.ScheduledStatusActivity +import com.keylesspalace.tusky.components.search.SearchActivity +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper +import com.keylesspalace.tusky.components.systemnotifications.disableAllNotifications +import com.keylesspalace.tusky.components.systemnotifications.enablePushNotificationsWithFallback +import com.keylesspalace.tusky.components.systemnotifications.showMigrationNoticeIfNecessary +import com.keylesspalace.tusky.components.trending.TrendingActivity +import com.keylesspalace.tusky.databinding.ActivityMainBinding +import com.keylesspalace.tusky.db.DraftsAlert +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.di.ApplicationScope +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.interfaces.AccountSelectionListener +import com.keylesspalace.tusky.interfaces.ActionButtonActivity +import com.keylesspalace.tusky.interfaces.FabFragment +import com.keylesspalace.tusky.interfaces.ReselectableFragment +import com.keylesspalace.tusky.pager.MainPagerAdapter +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.usecase.DeveloperToolsUseCase +import com.keylesspalace.tusky.usecase.LogoutUsecase +import com.keylesspalace.tusky.util.ActivityConstants +import com.keylesspalace.tusky.util.ShareShortcutHelper +import com.keylesspalace.tusky.util.deleteStaleCachedMedia +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.getDimension +import com.keylesspalace.tusky.util.getParcelableExtraCompat +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.overrideActivityTransitionCompat +import com.keylesspalace.tusky.util.reduceSwipeSensitivity +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import com.keylesspalace.tusky.util.viewBinding +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import com.mikepenz.materialdrawer.holder.BadgeStyle +import com.mikepenz.materialdrawer.holder.ColorHolder +import com.mikepenz.materialdrawer.holder.StringHolder +import com.mikepenz.materialdrawer.iconics.iconicsIcon +import com.mikepenz.materialdrawer.model.AbstractDrawerItem +import com.mikepenz.materialdrawer.model.DividerDrawerItem +import com.mikepenz.materialdrawer.model.PrimaryDrawerItem +import com.mikepenz.materialdrawer.model.ProfileDrawerItem +import com.mikepenz.materialdrawer.model.ProfileSettingDrawerItem +import com.mikepenz.materialdrawer.model.SecondaryDrawerItem +import com.mikepenz.materialdrawer.model.interfaces.IProfile +import com.mikepenz.materialdrawer.model.interfaces.descriptionRes +import com.mikepenz.materialdrawer.model.interfaces.descriptionText +import com.mikepenz.materialdrawer.model.interfaces.iconRes +import com.mikepenz.materialdrawer.model.interfaces.iconUrl +import com.mikepenz.materialdrawer.model.interfaces.nameRes +import com.mikepenz.materialdrawer.model.interfaces.nameText +import com.mikepenz.materialdrawer.util.AbstractDrawerImageLoader +import com.mikepenz.materialdrawer.util.DrawerImageLoader +import com.mikepenz.materialdrawer.util.addItems +import com.mikepenz.materialdrawer.util.addItemsAtPosition +import com.mikepenz.materialdrawer.util.updateBadge +import com.mikepenz.materialdrawer.widget.AccountHeaderView +import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.migration.OptionalInject +import de.c1710.filemojicompat_ui.helpers.EMOJI_PREFERENCE +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@OptionalInject +@AndroidEntryPoint +class MainActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider { + + @Inject + lateinit var eventHub: EventHub + + @Inject + lateinit var cacheUpdater: CacheUpdater + + @Inject + lateinit var logoutUsecase: LogoutUsecase + + @Inject + lateinit var draftsAlert: DraftsAlert + + @Inject + lateinit var developerToolsUseCase: DeveloperToolsUseCase + + @Inject + lateinit var shareShortcutHelper: ShareShortcutHelper + + @Inject + @ApplicationScope + lateinit var externalScope: CoroutineScope + + private val binding by viewBinding(ActivityMainBinding::inflate) + + private lateinit var header: AccountHeaderView + + private var onTabSelectedListener: OnTabSelectedListener? = null + + private var unreadAnnouncementsCount = 0 + + // We need to know if the emoji pack has been changed + private var selectedEmojiPack: String? = null + + /** Mediate between binding.viewPager and the chosen tab layout */ + private var tabLayoutMediator: TabLayoutMediator? = null + + /** Adapter for the different timeline tabs */ + private lateinit var tabAdapter: MainPagerAdapter + + private var directMessageTab: TabLayout.Tab? = null + + private val onBackPressedCallback = object : OnBackPressedCallback(false) { + override fun handleOnBackPressed() { + binding.viewPager.currentItem = 0 + } + } + + @SuppressLint("RestrictedApi") + override fun onCreate(savedInstanceState: Bundle?) { + // Newer Android versions don't need to install the compat Splash Screen + // and it can cause theming bugs. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + installSplashScreen() + } + super.onCreate(savedInstanceState) + + // will be redirected to LoginActivity by BaseActivity + val activeAccount = accountManager.activeAccount ?: return + + if (explodeAnimationWasRequested()) { + overrideActivityTransitionCompat( + ActivityConstants.OVERRIDE_TRANSITION_OPEN, + R.anim.explode, + R.anim.activity_open_exit + ) + } + + var showNotificationTab = false + + // check for savedInstanceState in order to not handle intent events more than once + if (intent != null && savedInstanceState == null) { + val notificationId = intent.getIntExtra(NOTIFICATION_ID, -1) + if (notificationId != -1) { + // opened from a notification action, cancel the notification + val notificationManager = getSystemService( + NOTIFICATION_SERVICE + ) as NotificationManager + notificationManager.cancel(intent.getStringExtra(NOTIFICATION_TAG), notificationId) + } + + /** there are two possibilities the accountId can be passed to MainActivity: + * - from our code as Long Intent Extra TUSKY_ACCOUNT_ID + * - from share shortcuts as String 'android.intent.extra.shortcut.ID' + */ + var tuskyAccountId = intent.getLongExtra(TUSKY_ACCOUNT_ID, -1) + if (tuskyAccountId == -1L) { + val accountIdString = intent.getStringExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID) + if (accountIdString != null) { + tuskyAccountId = accountIdString.toLong() + } + } + val accountRequested = tuskyAccountId != -1L + if (accountRequested && tuskyAccountId != activeAccount.id) { + accountManager.setActiveAccount(tuskyAccountId) + } + + val openDrafts = intent.getBooleanExtra(OPEN_DRAFTS, false) + + if (canHandleMimeType(intent.type) || intent.hasExtra(COMPOSE_OPTIONS)) { + // Sharing to Tusky from an external app + if (accountRequested) { + // The correct account is already active + forwardToComposeActivity(intent) + } else { + // No account was provided, show the chooser + showAccountChooserDialog( + getString(R.string.action_share_as), + true, + object : AccountSelectionListener { + override fun onAccountSelected(account: AccountEntity) { + val requestedId = account.id + if (requestedId == activeAccount.id) { + // The correct account is already active + forwardToComposeActivity(intent) + } else { + // A different account was requested, restart the activity + intent.putExtra(TUSKY_ACCOUNT_ID, requestedId) + changeAccount(requestedId, intent) + } + } + } + ) + } + } else if (openDrafts) { + val intent = DraftsActivity.newIntent(this) + startActivity(intent) + } else if (accountRequested && intent.hasExtra(NOTIFICATION_TYPE)) { + // user clicked a notification, show follow requests for type FOLLOW_REQUEST, + // otherwise show notification tab + if (intent.getStringExtra(NOTIFICATION_TYPE) == Notification.Type.FOLLOW_REQUEST.name) { + val intent = AccountListActivity.newIntent( + this, + AccountListActivity.Type.FOLLOW_REQUESTS + ) + startActivityWithSlideInAnimation(intent) + } else { + showNotificationTab = true + } + } + } + window.statusBarColor = Color.TRANSPARENT // don't draw a status bar, the DrawerLayout and the MaterialDrawerLayout have their own + setContentView(binding.root) + + binding.composeButton.setOnClickListener { + val composeIntent = Intent(applicationContext, ComposeActivity::class.java) + startActivity(composeIntent) + } + + // Determine which of the three toolbars should be the supportActionBar (which hosts + // the options menu). + val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false) + if (hideTopToolbar) { + when (preferences.getString(PrefKeys.MAIN_NAV_POSITION, "top")) { + "top" -> setSupportActionBar(binding.topNav) + "bottom" -> setSupportActionBar(binding.bottomNav) + } + binding.mainToolbar.hide() + // There's not enough space in the top/bottom bars to show the title as well. + supportActionBar?.setDisplayShowTitleEnabled(false) + } else { + setSupportActionBar(binding.mainToolbar) + binding.mainToolbar.show() + } + + loadDrawerAvatar(activeAccount.profilePictureUrl, true) + + addMenuProvider(this) + + binding.viewPager.reduceSwipeSensitivity() + + setupDrawer( + savedInstanceState, + addSearchButton = hideTopToolbar, + addTrendingTagsButton = !accountManager.activeAccount!!.tabPreferences.hasTab( + TRENDING_TAGS + ), + addTrendingStatusesButton = !accountManager.activeAccount!!.tabPreferences.hasTab( + TRENDING_STATUSES + ) + ) + + /* Fetch user info while we're doing other things. This has to be done after setting up the + * drawer, though, because its callback touches the header in the drawer. */ + fetchUserInfo() + + fetchAnnouncements() + + // Initialise the tab adapter and set to viewpager. Fragments appear to be leaked if the + // adapter changes over the life of the viewPager (the adapter, not its contents), so set + // the initial list of tabs to empty, and set the full list later in setupTabs(). See + // https://github.com/tuskyapp/Tusky/issues/3251 for details. + tabAdapter = MainPagerAdapter(emptyList(), this) + binding.viewPager.adapter = tabAdapter + + setupTabs(showNotificationTab) + + lifecycleScope.launch { + eventHub.events.collect { event -> + when (event) { + is ProfileEditedEvent -> onFetchUserInfoSuccess(event.newProfileData) + is MainTabsChangedEvent -> { + refreshMainDrawerItems( + addSearchButton = hideTopToolbar, + addTrendingTagsButton = !event.newTabs.hasTab(TRENDING_TAGS), + addTrendingStatusesButton = !event.newTabs.hasTab(TRENDING_STATUSES) + ) + + setupTabs(false) + } + is AnnouncementReadEvent -> { + unreadAnnouncementsCount-- + updateAnnouncementsBadge() + } + is NewNotificationsEvent -> { + directMessageTab?.let { + if (event.accountId == activeAccount.accountId) { + val hasDirectMessageNotification = + event.notifications.any { + it.type == Notification.Type.MENTION && it.status?.visibility == Status.Visibility.DIRECT + } + + if (hasDirectMessageNotification) { + showDirectMessageBadge(true) + } + } + } + } + is NotificationsLoadingEvent -> { + if (event.accountId == activeAccount.accountId) { + showDirectMessageBadge(false) + } + } + is ConversationsLoadingEvent -> { + if (event.accountId == activeAccount.accountId) { + showDirectMessageBadge(false) + } + } + } + } + } + + externalScope.launch(Dispatchers.IO) { + // Flush old media that was cached for sharing + deleteStaleCachedMedia(applicationContext.getExternalFilesDir("Tusky")) + } + + selectedEmojiPack = preferences.getString(EMOJI_PREFERENCE, "") + + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + + if ( + Build.VERSION.SDK_INT >= 33 && + ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED + ) { + ActivityCompat.requestPermissions( + this, + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + 1 + ) + } + + // "Post failed" dialog should display in this activity + draftsAlert.observeInContext(this, true) + } + + private fun showDirectMessageBadge(showBadge: Boolean) { + directMessageTab?.let { tab -> + tab.badge?.isVisible = showBadge + + // TODO a bit cumbersome (also for resetting) + lifecycleScope.launch(Dispatchers.IO) { + accountManager.activeAccount?.let { + if (it.hasDirectMessageBadge != showBadge) { + it.hasDirectMessageBadge = showBadge + accountManager.saveAccount(it) + } + } + } + } + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.activity_main, menu) + menu.findItem(R.id.action_search)?.apply { + icon = IconicsDrawable(this@MainActivity, GoogleMaterial.Icon.gmd_search).apply { + sizeDp = 20 + colorInt = MaterialColors.getColor(binding.mainToolbar, android.R.attr.textColorPrimary) + } + } + } + + override fun onPrepareMenu(menu: Menu) { + super.onPrepareMenu(menu) + + // If the main toolbar is hidden then there's no space in the top/bottomNav to show + // the menu items as icons, so forceably disable them + if (!binding.mainToolbar.isVisible) { + menu.forEach { + it.setShowAsAction( + SHOW_AS_ACTION_NEVER + ) + } + } + } + + override fun onMenuItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.action_search -> { + startActivity(SearchActivity.getIntent(this@MainActivity)) + true + } + else -> super.onOptionsItemSelected(item) + } + } + + override fun onResume() { + super.onResume() + val currentEmojiPack = preferences.getString(EMOJI_PREFERENCE, "") + if (currentEmojiPack != selectedEmojiPack) { + Log.d( + TAG, + "onResume: EmojiPack has been changed from %s to %s" + .format(selectedEmojiPack, currentEmojiPack) + ) + selectedEmojiPack = currentEmojiPack + recreate() + } + } + + override fun dispatchKeyEvent(event: KeyEvent): Boolean { + // Allow software back press to be properly dispatched to drawer layout + val handled = when (event.action) { + KeyEvent.ACTION_DOWN -> binding.mainDrawerLayout.onKeyDown(event.keyCode, event) + KeyEvent.ACTION_UP -> binding.mainDrawerLayout.onKeyUp(event.keyCode, event) + else -> false + } + return handled || super.dispatchKeyEvent(event) + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + when (keyCode) { + KeyEvent.KEYCODE_MENU -> { + if (binding.mainDrawerLayout.isOpen) { + binding.mainDrawerLayout.close() + } else { + binding.mainDrawerLayout.open() + } + return true + } + KeyEvent.KEYCODE_SEARCH -> { + startActivityWithSlideInAnimation(SearchActivity.getIntent(this)) + return true + } + } + if (event.isCtrlPressed || event.isShiftPressed) { + // FIXME: blackberry keyONE raises SHIFT key event even CTRL IS PRESSED + when (keyCode) { + KeyEvent.KEYCODE_N -> { + // open compose activity by pressing SHIFT + N (or CTRL + N) + val composeIntent = Intent(applicationContext, ComposeActivity::class.java) + startActivity(composeIntent) + return true + } + } + } + return super.onKeyDown(keyCode, event) + } + + public override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) + + if (intent != null) { + val redirectUrl = intent.getStringExtra(REDIRECT_URL) + if (redirectUrl != null) { + viewUrl(redirectUrl, PostLookupFallbackBehavior.DISPLAY_ERROR) + } + } + } + + private fun forwardToComposeActivity(intent: Intent) { + val composeOptions = + intent.getParcelableExtraCompat<ComposeActivity.ComposeOptions>(COMPOSE_OPTIONS) + + val composeIntent = if (composeOptions != null) { + ComposeActivity.startIntent(this, composeOptions) + } else { + Intent(this, ComposeActivity::class.java).apply { + action = intent.action + type = intent.type + putExtras(intent) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + } + startActivity(composeIntent) + finish() + } + + private fun setupDrawer( + savedInstanceState: Bundle?, + addSearchButton: Boolean, + addTrendingTagsButton: Boolean, + addTrendingStatusesButton: Boolean + ) { + val drawerOpenClickListener = View.OnClickListener { binding.mainDrawerLayout.open() } + + binding.mainToolbar.setNavigationOnClickListener(drawerOpenClickListener) + binding.topNav.setNavigationOnClickListener(drawerOpenClickListener) + binding.bottomNav.setNavigationOnClickListener(drawerOpenClickListener) + + header = AccountHeaderView(this).apply { + headerBackgroundScaleType = ImageView.ScaleType.CENTER_CROP + currentHiddenInList = true + onAccountHeaderListener = { _: View?, profile: IProfile, current: Boolean -> handleProfileClick(profile, current) } + addProfile( + ProfileSettingDrawerItem().apply { + identifier = DRAWER_ITEM_ADD_ACCOUNT + nameRes = R.string.add_account_name + descriptionRes = R.string.add_account_description + iconicsIcon = GoogleMaterial.Icon.gmd_add + }, + 0 + ) + attachToSliderView(binding.mainDrawer) + dividerBelowHeader = false + closeDrawerOnProfileListClick = true + } + + header.currentProfileName.maxLines = 1 + header.currentProfileName.ellipsize = TextUtils.TruncateAt.END + + header.accountHeaderBackground.setColorFilter(getColor(R.color.headerBackgroundFilter)) + header.accountHeaderBackground.setBackgroundColor( + MaterialColors.getColor(header, R.attr.colorBackgroundAccent) + ) + val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) + + DrawerImageLoader.init(object : AbstractDrawerImageLoader() { + override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String?) { + if (animateAvatars) { + Glide.with(imageView) + .load(uri) + .placeholder(placeholder) + .into(imageView) + } else { + Glide.with(imageView) + .asBitmap() + .load(uri) + .placeholder(placeholder) + .into(imageView) + } + } + + override fun cancel(imageView: ImageView) { + // nothing to do, Glide already handles cancellation automatically + } + + override fun placeholder(ctx: Context, tag: String?): Drawable { + if (tag == DrawerImageLoader.Tags.PROFILE.name || tag == DrawerImageLoader.Tags.PROFILE_DRAWER_ITEM.name) { + return AppCompatResources.getDrawable(ctx, R.drawable.avatar_default)!! + } + + return super.placeholder(ctx, tag) + } + }) + + binding.mainDrawer.apply { + refreshMainDrawerItems( + addSearchButton = addSearchButton, + addTrendingTagsButton = addTrendingTagsButton, + addTrendingStatusesButton = addTrendingStatusesButton + ) + setSavedInstance(savedInstanceState) + } + } + + private fun refreshMainDrawerItems( + addSearchButton: Boolean, + addTrendingTagsButton: Boolean, + addTrendingStatusesButton: Boolean + ) { + binding.mainDrawer.apply { + itemAdapter.clear() + tintStatusBar = true + addItems( + primaryDrawerItem { + nameRes = R.string.action_edit_profile + iconicsIcon = GoogleMaterial.Icon.gmd_person + onClick = { + val intent = Intent(context, EditProfileActivity::class.java) + startActivityWithSlideInAnimation(intent) + } + }, + primaryDrawerItem { + nameRes = R.string.action_view_favourites + isSelectable = false + iconicsIcon = GoogleMaterial.Icon.gmd_star + onClick = { + val intent = StatusListActivity.newFavouritesIntent(context) + startActivityWithSlideInAnimation(intent) + } + }, + primaryDrawerItem { + nameRes = R.string.action_view_bookmarks + iconicsIcon = GoogleMaterial.Icon.gmd_bookmark + onClick = { + val intent = StatusListActivity.newBookmarksIntent(context) + startActivityWithSlideInAnimation(intent) + } + }, + primaryDrawerItem { + nameRes = R.string.action_view_follow_requests + iconicsIcon = GoogleMaterial.Icon.gmd_person_add + onClick = { + val intent = AccountListActivity.newIntent(context, AccountListActivity.Type.FOLLOW_REQUESTS) + startActivityWithSlideInAnimation(intent) + } + }, + primaryDrawerItem { + nameRes = R.string.action_lists + iconicsIcon = GoogleMaterial.Icon.gmd_list + onClick = { + startActivityWithSlideInAnimation(ListsActivity.newIntent(context)) + } + }, + primaryDrawerItem { + nameRes = R.string.action_access_drafts + iconRes = R.drawable.ic_notebook + onClick = { + val intent = DraftsActivity.newIntent(context) + startActivityWithSlideInAnimation(intent) + } + }, + primaryDrawerItem { + nameRes = R.string.action_access_scheduled_posts + iconRes = R.drawable.ic_access_time + onClick = { + startActivityWithSlideInAnimation(ScheduledStatusActivity.newIntent(context)) + } + }, + primaryDrawerItem { + identifier = DRAWER_ITEM_ANNOUNCEMENTS + nameRes = R.string.title_announcements + iconRes = R.drawable.ic_bullhorn_24dp + onClick = { + startActivityWithSlideInAnimation(AnnouncementsActivity.newIntent(context)) + } + badgeStyle = BadgeStyle().apply { + textColor = ColorHolder.fromColor(MaterialColors.getColor(binding.mainDrawer, materialR.attr.colorOnPrimary)) + color = ColorHolder.fromColor(MaterialColors.getColor(binding.mainDrawer, materialR.attr.colorPrimary)) + } + }, + DividerDrawerItem(), + secondaryDrawerItem { + nameRes = R.string.action_view_account_preferences + iconRes = R.drawable.ic_account_settings + onClick = { + val intent = PreferencesActivity.newIntent(context, PreferencesActivity.ACCOUNT_PREFERENCES) + startActivityWithSlideInAnimation(intent) + } + }, + secondaryDrawerItem { + nameRes = R.string.action_view_preferences + iconicsIcon = GoogleMaterial.Icon.gmd_settings + onClick = { + val intent = PreferencesActivity.newIntent(context, PreferencesActivity.GENERAL_PREFERENCES) + startActivityWithSlideInAnimation(intent) + } + }, + secondaryDrawerItem { + nameRes = R.string.about_title_activity + iconicsIcon = GoogleMaterial.Icon.gmd_info + onClick = { + val intent = Intent(context, AboutActivity::class.java) + startActivityWithSlideInAnimation(intent) + } + }, + secondaryDrawerItem { + nameRes = R.string.action_logout + iconRes = R.drawable.ic_logout + onClick = ::logout + } + ) + + if (addSearchButton) { + binding.mainDrawer.addItemsAtPosition( + 4, + primaryDrawerItem { + nameRes = R.string.action_search + iconicsIcon = GoogleMaterial.Icon.gmd_search + onClick = { + startActivityWithSlideInAnimation(SearchActivity.getIntent(context)) + } + } + ) + } + + if (addTrendingTagsButton) { + binding.mainDrawer.addItemsAtPosition( + 5, + primaryDrawerItem { + nameRes = R.string.title_public_trending_hashtags + iconicsIcon = GoogleMaterial.Icon.gmd_trending_up + onClick = { + startActivityWithSlideInAnimation(TrendingActivity.getIntent(context)) + } + } + ) + } + + if (addTrendingStatusesButton) { + binding.mainDrawer.addItemsAtPosition( + 6, + primaryDrawerItem { + nameRes = R.string.title_public_trending_statuses + iconicsIcon = GoogleMaterial.Icon.gmd_local_fire_department + onClick = { + startActivityWithSlideInAnimation(StatusListActivity.newTrendingIntent(context)) + } + } + ) + } + } + + if (BuildConfig.DEBUG) { + // Add a "Developer tools" entry. Code that makes it easier to + // set the app state at runtime belongs here, it will never + // be exposed to users. + binding.mainDrawer.addItems( + DividerDrawerItem(), + secondaryDrawerItem { + nameText = "Developer tools" + isEnabled = true + iconicsIcon = GoogleMaterial.Icon.gmd_developer_mode + onClick = { + buildDeveloperToolsDialog().show() + } + } + ) + } + } + + private fun buildDeveloperToolsDialog(): AlertDialog { + return AlertDialog.Builder(this) + .setTitle("Developer Tools") + .setItems( + arrayOf("Create \"Load more\" gap") + ) { _, which -> + Log.d(TAG, "Developer tools: $which") + when (which) { + 0 -> { + Log.d(TAG, "Creating \"Load more\" gap") + lifecycleScope.launch { + accountManager.activeAccount?.let { + developerToolsUseCase.createLoadMoreGap( + it.id + ) + } + } + } + } + } + .create() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(binding.mainDrawer.saveInstanceState(outState)) + } + + private fun setupTabs(selectNotificationTab: Boolean) { + val activeTabLayout = if (preferences.getString(PrefKeys.MAIN_NAV_POSITION, "top") == "bottom") { + val actionBarSize = getDimension(this, androidx.appcompat.R.attr.actionBarSize) + val fabMargin = resources.getDimensionPixelSize(R.dimen.fabMargin) + (binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = actionBarSize + fabMargin + binding.topNav.hide() + binding.bottomTabLayout + } else { + binding.bottomNav.hide() + (binding.viewPager.layoutParams as CoordinatorLayout.LayoutParams).bottomMargin = 0 + (binding.composeButton.layoutParams as CoordinatorLayout.LayoutParams).anchorId = R.id.viewPager + binding.tabLayout + } + + // Save the previous tab so it can be restored later + val previousTab = tabAdapter.tabs.getOrNull(binding.viewPager.currentItem) + + val tabs = accountManager.activeAccount!!.tabPreferences + + // Detach any existing mediator before changing tab contents and attaching a new mediator + tabLayoutMediator?.detach() + + directMessageTab = null + + tabAdapter.tabs = tabs + tabAdapter.notifyItemRangeChanged(0, tabs.size) + + tabLayoutMediator = TabLayoutMediator(activeTabLayout, binding.viewPager, true) { + tab: TabLayout.Tab, position: Int -> + tab.icon = AppCompatResources.getDrawable(this@MainActivity, tabs[position].icon) + tab.contentDescription = when (tabs[position].id) { + LIST -> tabs[position].arguments[1] + else -> getString(tabs[position].text) + } + if (tabs[position].id == DIRECT) { + val badge = tab.orCreateBadge + badge.isVisible = accountManager.activeAccount?.hasDirectMessageBadge ?: false + badge.backgroundColor = MaterialColors.getColor(binding.mainDrawer, materialR.attr.colorPrimary) + directMessageTab = tab + } + }.also { it.attach() } + + // Selected tab is either + // - Notification tab (if appropriate) + // - The previously selected tab (if it hasn't been removed) + // - Left-most tab + val position = if (selectNotificationTab) { + tabs.indexOfFirst { it.id == NOTIFICATIONS } + } else { + previousTab?.let { tabs.indexOfFirst { it == previousTab } } + }.takeIf { it != -1 } ?: 0 + binding.viewPager.setCurrentItem(position, false) + + val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin) + binding.viewPager.setPageTransformer(MarginPageTransformer(pageMargin)) + + val enableSwipeForTabs = preferences.getBoolean(PrefKeys.ENABLE_SWIPE_FOR_TABS, true) + binding.viewPager.isUserInputEnabled = enableSwipeForTabs + + onTabSelectedListener?.let { + activeTabLayout.removeOnTabSelectedListener(it) + } + + onTabSelectedListener = object : OnTabSelectedListener { + override fun onTabSelected(tab: TabLayout.Tab) { + onBackPressedCallback.isEnabled = tab.position > 0 + + binding.mainToolbar.title = tab.contentDescription + + refreshComposeButtonState(tabAdapter, tab.position) + + if (tab == directMessageTab) { + tab.badge?.isVisible = false + + accountManager.activeAccount?.let { + if (it.hasDirectMessageBadge) { + it.hasDirectMessageBadge = false + accountManager.saveAccount(it) + } + } + } + } + + override fun onTabUnselected(tab: TabLayout.Tab) {} + + override fun onTabReselected(tab: TabLayout.Tab) { + val fragment = tabAdapter.getFragment(tab.position) + if (fragment is ReselectableFragment) { + (fragment as ReselectableFragment).onReselect() + } + + refreshComposeButtonState(tabAdapter, tab.position) + } + }.also { + activeTabLayout.addOnTabSelectedListener(it) + } + + supportActionBar?.title = tabs[position].title(this@MainActivity) + binding.mainToolbar.setOnClickListener { + ( + tabAdapter.getFragment( + activeTabLayout.selectedTabPosition + ) as? ReselectableFragment + )?.onReselect() + } + + updateProfiles() + } + + private fun refreshComposeButtonState(adapter: MainPagerAdapter, tabPosition: Int) { + adapter.getFragment(tabPosition)?.also { fragment -> + if (fragment is FabFragment) { + if (fragment.isFabVisible()) { + binding.composeButton.show() + } else { + binding.composeButton.hide() + } + } else { + binding.composeButton.show() + } + } + } + + private fun handleProfileClick(profile: IProfile, current: Boolean): Boolean { + val activeAccount = accountManager.activeAccount + + // open profile when active image was clicked + if (current && activeAccount != null) { + val intent = AccountActivity.getIntent(this, activeAccount.accountId) + startActivityWithSlideInAnimation(intent) + return false + } + // open LoginActivity to add new account + if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) { + startActivityWithSlideInAnimation( + LoginActivity.getIntent(this, LoginActivity.MODE_ADDITIONAL_LOGIN) + ) + return false + } + // change Account + changeAccount(profile.identifier, null) + return false + } + + private fun changeAccount(newSelectedId: Long, forward: Intent?) { + cacheUpdater.stop() + accountManager.setActiveAccount(newSelectedId) + val intent = Intent(this, MainActivity::class.java) + intent.putExtra(OPEN_WITH_EXPLODE_ANIMATION, true) + if (forward != null) { + intent.type = forward.type + intent.action = forward.action + intent.putExtras(forward) + } + startActivity(intent) + finish() + } + + private fun logout() { + accountManager.activeAccount?.let { activeAccount -> + AlertDialog.Builder(this) + .setTitle(R.string.action_logout) + .setMessage(getString(R.string.action_logout_confirm, activeAccount.fullName)) + .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> + binding.appBar.hide() + binding.viewPager.hide() + binding.progressBar.show() + binding.bottomNav.hide() + binding.composeButton.hide() + + lifecycleScope.launch { + val otherAccountAvailable = logoutUsecase.logout() + val intent = if (otherAccountAvailable) { + Intent(this@MainActivity, MainActivity::class.java) + } else { + LoginActivity.getIntent(this@MainActivity, LoginActivity.MODE_DEFAULT) + } + startActivity(intent) + finish() + } + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + } + + private fun fetchUserInfo() = lifecycleScope.launch { + mastodonApi.accountVerifyCredentials().fold( + { userInfo -> + onFetchUserInfoSuccess(userInfo) + }, + { throwable -> + Log.e(TAG, "Failed to fetch user info. " + throwable.message) + } + ) + } + + private fun onFetchUserInfoSuccess(me: Account) { + Glide.with(header.accountHeaderBackground) + .asBitmap() + .load(me.header) + .into(header.accountHeaderBackground) + + loadDrawerAvatar(me.avatar, false) + + accountManager.updateActiveAccount(me) + NotificationHelper.createNotificationChannelsForAccount( + accountManager.activeAccount!!, + this + ) + + // Setup push notifications + showMigrationNoticeIfNecessary( + this, + binding.mainCoordinatorLayout, + binding.composeButton, + accountManager + ) + if (NotificationHelper.areNotificationsEnabled(this, accountManager)) { + lifecycleScope.launch { + enablePushNotificationsWithFallback(this@MainActivity, mastodonApi, accountManager) + } + } else { + disableAllNotifications(this, accountManager) + } + + updateProfiles() + shareShortcutHelper.updateShortcuts() + } + + @SuppressLint("CheckResult") + private fun loadDrawerAvatar(avatarUrl: String, showPlaceholder: Boolean) { + val hideTopToolbar = preferences.getBoolean(PrefKeys.HIDE_TOP_TOOLBAR, false) + val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) + + val activeToolbar = if (hideTopToolbar) { + val navOnBottom = preferences.getString(PrefKeys.MAIN_NAV_POSITION, "top") == "bottom" + if (navOnBottom) { + binding.bottomNav + } else { + binding.topNav + } + } else { + binding.mainToolbar + } + + val navIconSize = resources.getDimensionPixelSize(R.dimen.avatar_toolbar_nav_icon_size) + + if (animateAvatars) { + Glide.with(this) + .asDrawable() + .load(avatarUrl) + .transform( + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)) + ) + .apply { + if (showPlaceholder) placeholder(R.drawable.avatar_default) + } + .into(object : CustomTarget<Drawable>(navIconSize, navIconSize) { + + override fun onLoadStarted(placeholder: Drawable?) { + placeholder?.let { + activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize) + } + } + + override fun onResourceReady( + resource: Drawable, + transition: Transition<in Drawable>? + ) { + if (resource is Animatable) resource.start() + activeToolbar.navigationIcon = FixedSizeDrawable(resource, navIconSize, navIconSize) + } + + override fun onLoadCleared(placeholder: Drawable?) { + placeholder?.let { + activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize) + } + } + }) + } else { + Glide.with(this) + .asBitmap() + .load(avatarUrl) + .transform( + RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp)) + ) + .apply { + if (showPlaceholder) placeholder(R.drawable.avatar_default) + } + .into(object : CustomTarget<Bitmap>(navIconSize, navIconSize) { + override fun onLoadStarted(placeholder: Drawable?) { + placeholder?.let { + activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize) + } + } + + override fun onResourceReady( + resource: Bitmap, + transition: Transition<in Bitmap>? + ) { + activeToolbar.navigationIcon = FixedSizeDrawable( + BitmapDrawable(resources, resource), + navIconSize, + navIconSize + ) + } + + override fun onLoadCleared(placeholder: Drawable?) { + placeholder?.let { + activeToolbar.navigationIcon = FixedSizeDrawable(it, navIconSize, navIconSize) + } + } + }) + } + } + + private fun fetchAnnouncements() { + lifecycleScope.launch { + mastodonApi.listAnnouncements(false) + .fold( + { announcements -> + unreadAnnouncementsCount = announcements.count { !it.read } + updateAnnouncementsBadge() + }, + { throwable -> + Log.w(TAG, "Failed to fetch announcements.", throwable) + } + ) + } + } + + private fun updateAnnouncementsBadge() { + binding.mainDrawer.updateBadge( + DRAWER_ITEM_ANNOUNCEMENTS, + StringHolder( + if (unreadAnnouncementsCount <= 0) null else unreadAnnouncementsCount.toString() + ) + ) + } + + private fun updateProfiles() { + val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + val profiles: MutableList<IProfile> = + accountManager.getAllAccountsOrderedByActive().map { acc -> + ProfileDrawerItem().apply { + isSelected = acc.isActive + nameText = acc.displayName.emojify(acc.emojis, header, animateEmojis) + iconUrl = acc.profilePictureUrl + isNameShown = true + identifier = acc.id + descriptionText = acc.fullName + } + }.toMutableList() + + // reuse the already existing "add account" item + for (profile in header.profiles.orEmpty()) { + if (profile.identifier == DRAWER_ITEM_ADD_ACCOUNT) { + profiles.add(profile) + break + } + } + header.clear() + header.profiles = profiles + header.setActiveProfile(accountManager.activeAccount!!.id) + binding.mainToolbar.subtitle = if (accountManager.shouldDisplaySelfUsername()) { + accountManager.activeAccount!!.fullName + } else { + null + } + } + + private fun explodeAnimationWasRequested(): Boolean { + return intent.getBooleanExtra(OPEN_WITH_EXPLODE_ANIMATION, false) + } + + override fun getActionButton() = binding.composeButton + + companion object { + const val OPEN_WITH_EXPLODE_ANIMATION = "explode" + + private const val TAG = "MainActivity" // logging tag + private const val DRAWER_ITEM_ADD_ACCOUNT: Long = -13 + private const val DRAWER_ITEM_ANNOUNCEMENTS: Long = 14 + private const val REDIRECT_URL = "redirectUrl" + private const val OPEN_DRAFTS = "draft" + private const val TUSKY_ACCOUNT_ID = "tuskyAccountId" + private const val COMPOSE_OPTIONS = "composeOptions" + private const val NOTIFICATION_TYPE = "notificationType" + private const val NOTIFICATION_TAG = "notificationTag" + private const val NOTIFICATION_ID = "notificationId" + + /** + * Switches the active account to the provided accountId and then stays on MainActivity + */ + @JvmStatic + fun accountSwitchIntent(context: Context, tuskyAccountId: Long): Intent { + return Intent(context, MainActivity::class.java).apply { + putExtra(TUSKY_ACCOUNT_ID, tuskyAccountId) + } + } + + /** + * Switches the active account to the accountId and takes the user to the correct place according to the notification they clicked + */ + @JvmStatic + fun openNotificationIntent( + context: Context, + tuskyAccountId: Long, + type: Notification.Type + ): Intent { + return accountSwitchIntent(context, tuskyAccountId).apply { + putExtra(NOTIFICATION_TYPE, type.name) + } + } + + /** + * Switches the active account to the accountId and then opens ComposeActivity with the provided options + * @param tuskyAccountId the id of the Tusky account to open the screen with. Set to -1 for current account. + * @param notificationId optional id of the notification that should be cancelled when this intent is opened + * @param notificationTag optional tag of the notification that should be cancelled when this intent is opened + */ + @JvmStatic + fun composeIntent( + context: Context, + options: ComposeActivity.ComposeOptions, + tuskyAccountId: Long = -1, + notificationTag: String? = null, + notificationId: Int = -1 + ): Intent { + return accountSwitchIntent(context, tuskyAccountId).apply { + action = Intent.ACTION_SEND // so it can be opened via shortcuts + putExtra(COMPOSE_OPTIONS, options) + putExtra(NOTIFICATION_TAG, notificationTag) + putExtra(NOTIFICATION_ID, notificationId) + } + } + + /** + * switches the active account to the accountId and then tries to resolve and show the provided url + */ + @JvmStatic + fun redirectIntent(context: Context, tuskyAccountId: Long, url: String): Intent { + return accountSwitchIntent(context, tuskyAccountId).apply { + putExtra(REDIRECT_URL, url) + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + } + + /** + * switches the active account to the provided accountId and then opens drafts + */ + fun draftIntent(context: Context, tuskyAccountId: Long): Intent { + return accountSwitchIntent(context, tuskyAccountId).apply { + putExtra(OPEN_DRAFTS, true) + } + } + } +} + +private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem { + return PrimaryDrawerItem() + .apply { + isSelectable = false + isIconTinted = true + } + .apply(block) +} + +private inline fun secondaryDrawerItem(block: SecondaryDrawerItem.() -> Unit): SecondaryDrawerItem { + return SecondaryDrawerItem() + .apply { + isSelectable = false + isIconTinted = true + } + .apply(block) +} + +private var AbstractDrawerItem<*, *>.onClick: () -> Unit + get() = throw UnsupportedOperationException() + set(value) { + onDrawerItemClickListener = { _, _, _ -> + value() + false + } + } diff --git a/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt new file mode 100644 index 0000000..778be42 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/StatusListActivity.kt @@ -0,0 +1,435 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <https://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import androidx.fragment.app.commit +import androidx.lifecycle.lifecycleScope +import at.connyduck.calladapter.networkresult.fold +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.FilterUpdatedEvent +import com.keylesspalace.tusky.components.filters.EditFilterActivity +import com.keylesspalace.tusky.components.filters.FiltersActivity +import com.keylesspalace.tusky.components.timeline.TimelineFragment +import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel.Kind +import com.keylesspalace.tusky.databinding.ActivityStatuslistBinding +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.FilterV1 +import com.keylesspalace.tusky.util.isHttpNotFound +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import com.keylesspalace.tusky.util.viewBinding +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class StatusListActivity : BottomSheetActivity() { + + @Inject + lateinit var eventHub: EventHub + + private val binding: ActivityStatuslistBinding by viewBinding( + ActivityStatuslistBinding::inflate + ) + private lateinit var kind: Kind + private var hashtag: String? = null + private var followTagItem: MenuItem? = null + private var unfollowTagItem: MenuItem? = null + private var muteTagItem: MenuItem? = null + private var unmuteTagItem: MenuItem? = null + + /** The filter muting hashtag, null if unknown or hashtag is not filtered */ + private var mutedFilterV1: FilterV1? = null + private var mutedFilter: Filter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + Log.d("StatusListActivity", "onCreate") + super.onCreate(savedInstanceState) + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) + + kind = Kind.valueOf(intent.getStringExtra(EXTRA_KIND)!!) + val listId = intent.getStringExtra(EXTRA_LIST_ID) + hashtag = intent.getStringExtra(EXTRA_HASHTAG) + + val title = when (kind) { + Kind.FAVOURITES -> getString(R.string.title_favourites) + Kind.BOOKMARKS -> getString(R.string.title_bookmarks) + Kind.TAG -> getString(R.string.title_tag).format(hashtag) + Kind.PUBLIC_TRENDING_STATUSES -> getString(R.string.title_public_trending_statuses) + else -> intent.getStringExtra(EXTRA_LIST_TITLE) + } + + supportActionBar?.run { + setTitle(title) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + if (supportFragmentManager.findFragmentById(R.id.fragmentContainer) == null) { + supportFragmentManager.commit { + val fragment = if (kind == Kind.TAG) { + TimelineFragment.newHashtagInstance(listOf(hashtag!!)) + } else { + TimelineFragment.newInstance(kind, listId) + } + replace(R.id.fragmentContainer, fragment) + } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + val tag = hashtag + if (kind == Kind.TAG && tag != null) { + lifecycleScope.launch { + mastodonApi.tag(tag).fold( + { tagEntity -> + menuInflater.inflate(R.menu.view_hashtag_toolbar, menu) + followTagItem = menu.findItem(R.id.action_follow_hashtag) + unfollowTagItem = menu.findItem(R.id.action_unfollow_hashtag) + muteTagItem = menu.findItem(R.id.action_mute_hashtag) + unmuteTagItem = menu.findItem(R.id.action_unmute_hashtag) + followTagItem?.isVisible = tagEntity.following == false + unfollowTagItem?.isVisible = tagEntity.following == true + followTagItem?.setOnMenuItemClickListener { followTag() } + unfollowTagItem?.setOnMenuItemClickListener { unfollowTag() } + muteTagItem?.setOnMenuItemClickListener { muteTag() } + unmuteTagItem?.setOnMenuItemClickListener { unmuteTag() } + updateMuteTagMenuItems() + }, + { + Log.w(TAG, "Failed to query tag #$tag", it) + } + ) + } + } + + return super.onCreateOptionsMenu(menu) + } + + private fun followTag(): Boolean { + val tag = hashtag + if (tag != null) { + lifecycleScope.launch { + mastodonApi.followTag(tag).fold( + { + followTagItem?.isVisible = false + unfollowTagItem?.isVisible = true + + Snackbar.make( + binding.root, + getString(R.string.following_hashtag_success_format, tag), + Snackbar.LENGTH_SHORT + ).show() + }, + { + Snackbar.make( + binding.root, + getString(R.string.error_following_hashtag_format, tag), + Snackbar.LENGTH_SHORT + ).show() + Log.e(TAG, "Failed to follow #$tag", it) + } + ) + } + } + + return true + } + + private fun unfollowTag(): Boolean { + val tag = hashtag + if (tag != null) { + lifecycleScope.launch { + mastodonApi.unfollowTag(tag).fold( + { + followTagItem?.isVisible = true + unfollowTagItem?.isVisible = false + + Snackbar.make( + binding.root, + getString(R.string.unfollowing_hashtag_success_format, tag), + Snackbar.LENGTH_SHORT + ).show() + }, + { + Snackbar.make( + binding.root, + getString(R.string.error_unfollowing_hashtag_format, tag), + Snackbar.LENGTH_SHORT + ).show() + Log.e(TAG, "Failed to unfollow #$tag", it) + } + ) + } + } + + return true + } + + /** + * Determine if the current hashtag is muted, and update the UI state accordingly. + */ + private fun updateMuteTagMenuItems() { + val tag = hashtag ?: return + val hashedTag = "#$tag" + + muteTagItem?.isVisible = true + muteTagItem?.isEnabled = false + unmuteTagItem?.isVisible = false + + lifecycleScope.launch { + mastodonApi.getFilters().fold( + { filters -> + mutedFilter = filters.firstOrNull { filter -> + // TODO shouldn't this be an exact match (only one keyword; exactly the hashtag)? + filter.context.contains(Filter.Kind.HOME.kind) && filter.title == hashedTag + } + updateTagMuteState(mutedFilter != null) + }, + { throwable -> + if (throwable.isHttpNotFound()) { + mastodonApi.getFiltersV1().fold( + { filters -> + mutedFilterV1 = filters.firstOrNull { filter -> + hashedTag == filter.phrase && filter.context.contains(FilterV1.HOME) + } + updateTagMuteState(mutedFilterV1 != null) + }, + { throwable2 -> + Log.e(TAG, "Error getting filters: $throwable2") + } + ) + } else { + Log.e(TAG, "Error getting filters: $throwable") + } + } + ) + } + } + + private fun updateTagMuteState(muted: Boolean) { + if (muted) { + muteTagItem?.isVisible = false + muteTagItem?.isEnabled = false + unmuteTagItem?.isVisible = true + } else { + unmuteTagItem?.isVisible = false + muteTagItem?.isEnabled = true + muteTagItem?.isVisible = true + } + } + + private fun muteTag(): Boolean { + val tag = hashtag ?: return true + + lifecycleScope.launch { + var filterCreateSuccess = false + val hashedTag = "#$tag" + + mastodonApi.createFilter( + title = "#$tag", + context = listOf(FilterV1.HOME), + filterAction = Filter.Action.WARN.action, + expiresInSeconds = null + ).fold( + { filter -> + if (mastodonApi.addFilterKeyword( + filterId = filter.id, + keyword = hashedTag, + wholeWord = true + ).isSuccess + ) { + // must be requested again; otherwise does not contain the keyword (but server does) + mutedFilter = mastodonApi.getFilter(filter.id).getOrNull() + + eventHub.dispatch(FilterUpdatedEvent(filter.context)) + filterCreateSuccess = true + } else { + Snackbar.make( + binding.root, + getString(R.string.error_muting_hashtag_format, tag), + Snackbar.LENGTH_SHORT + ).show() + Log.e(TAG, "Failed to mute #$tag") + } + }, + { throwable -> + if (throwable.isHttpNotFound()) { + mastodonApi.createFilterV1( + hashedTag, + listOf(FilterV1.HOME), + irreversible = false, + wholeWord = true, + expiresInSeconds = null + ).fold( + { filter -> + mutedFilterV1 = filter + eventHub.dispatch(FilterUpdatedEvent(filter.context)) + filterCreateSuccess = true + }, + { throwable2 -> + Snackbar.make( + binding.root, + getString(R.string.error_muting_hashtag_format, tag), + Snackbar.LENGTH_SHORT + ).show() + Log.e(TAG, "Failed to mute #$tag", throwable2) + } + ) + } else { + Snackbar.make( + binding.root, + getString(R.string.error_muting_hashtag_format, tag), + Snackbar.LENGTH_SHORT + ).show() + Log.e(TAG, "Failed to mute #$tag", throwable) + } + } + ) + + if (filterCreateSuccess) { + updateTagMuteState(true) + Snackbar.make( + binding.root, + getString(R.string.muting_hashtag_success_format, tag), + Snackbar.LENGTH_LONG + ).apply { + setAction(R.string.action_view_filter) { + val intent = if (mutedFilter != null) { + Intent(this@StatusListActivity, EditFilterActivity::class.java).apply { + putExtra(EditFilterActivity.FILTER_TO_EDIT, mutedFilter) + } + } else { + Intent(this@StatusListActivity, FiltersActivity::class.java) + } + + startActivityWithSlideInAnimation(intent) + } + show() + } + } + } + + return true + } + + private fun unmuteTag(): Boolean { + lifecycleScope.launch { + val tag = hashtag + val result = if (mutedFilter != null) { + val filter = mutedFilter!! + if (filter.context.size > 1) { + // This filter exists in multiple contexts, just remove the home context + mastodonApi.updateFilter( + id = filter.id, + context = filter.context.filter { it != Filter.Kind.HOME.kind } + ) + } else { + mastodonApi.deleteFilter(filter.id) + } + } else if (mutedFilterV1 != null) { + mutedFilterV1?.let { filter -> + if (filter.context.size > 1) { + // This filter exists in multiple contexts, just remove the home context + mastodonApi.updateFilterV1( + id = filter.id, + phrase = filter.phrase, + context = filter.context.filter { it != FilterV1.HOME }, + irreversible = null, + wholeWord = null, + expiresInSeconds = null + ) + } else { + mastodonApi.deleteFilterV1(filter.id) + } + } + } else { + null + } + + result?.fold( + { + updateTagMuteState(false) + eventHub.dispatch(FilterUpdatedEvent(listOf(Filter.Kind.HOME.kind))) + mutedFilterV1 = null + mutedFilter = null + + Snackbar.make( + binding.root, + getString(R.string.unmuting_hashtag_success_format, tag), + Snackbar.LENGTH_SHORT + ).show() + }, + { throwable -> + Snackbar.make( + binding.root, + getString(R.string.error_unmuting_hashtag_format, tag), + Snackbar.LENGTH_SHORT + ).show() + Log.e(TAG, "Failed to unmute #$tag", throwable) + } + ) + } + + return true + } + + companion object { + + private const val EXTRA_KIND = "kind" + private const val EXTRA_LIST_ID = "id" + private const val EXTRA_LIST_TITLE = "title" + private const val EXTRA_HASHTAG = "tag" + const val TAG = "StatusListActivity" + + fun newFavouritesIntent(context: Context) = + Intent(context, StatusListActivity::class.java).apply { + putExtra(EXTRA_KIND, Kind.FAVOURITES.name) + } + + fun newBookmarksIntent(context: Context) = + Intent(context, StatusListActivity::class.java).apply { + putExtra(EXTRA_KIND, Kind.BOOKMARKS.name) + } + + fun newListIntent(context: Context, listId: String, listTitle: String) = + Intent(context, StatusListActivity::class.java).apply { + putExtra(EXTRA_KIND, Kind.LIST.name) + putExtra(EXTRA_LIST_ID, listId) + putExtra(EXTRA_LIST_TITLE, listTitle) + } + + @JvmStatic + fun newHashtagIntent(context: Context, hashtag: String) = + Intent(context, StatusListActivity::class.java).apply { + putExtra(EXTRA_KIND, Kind.TAG.name) + putExtra(EXTRA_HASHTAG, hashtag) + } + + fun newTrendingIntent(context: Context) = + Intent(context, StatusListActivity::class.java).apply { + putExtra(EXTRA_KIND, Kind.PUBLIC_TRENDING_STATUSES.name) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/TabData.kt b/app/src/main/java/com/keylesspalace/tusky/TabData.kt new file mode 100644 index 0000000..7dccd49 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/TabData.kt @@ -0,0 +1,155 @@ +/* Copyright 2019 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky + +import android.content.Context +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.fragment.app.Fragment +import com.keylesspalace.tusky.components.conversation.ConversationsFragment +import com.keylesspalace.tusky.components.notifications.NotificationsFragment +import com.keylesspalace.tusky.components.timeline.TimelineFragment +import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel +import com.keylesspalace.tusky.components.trending.TrendingTagsFragment +import java.util.Objects + +/** this would be a good case for a sealed class, but that does not work nice with Room */ + +const val HOME = "Home" +const val NOTIFICATIONS = "Notifications" +const val LOCAL = "Local" +const val FEDERATED = "Federated" +const val DIRECT = "Direct" +const val TRENDING_TAGS = "TrendingTags" +const val TRENDING_STATUSES = "TrendingStatuses" +const val HASHTAG = "Hashtag" +const val LIST = "List" +const val BOOKMARKS = "Bookmarks" + +data class TabData( + val id: String, + @StringRes val text: Int, + @DrawableRes val icon: Int, + val fragment: (List<String>) -> Fragment, + val arguments: List<String> = emptyList(), + val title: (Context) -> String = { context -> context.getString(text) } +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as TabData + + if (id != other.id) return false + return arguments == other.arguments + } + + override fun hashCode() = Objects.hash(id, arguments) +} + +fun List<TabData>.hasTab(id: String): Boolean = this.find { it.id == id } != null + +fun createTabDataFromId(id: String, arguments: List<String> = emptyList()): TabData { + return when (id) { + HOME -> TabData( + id = HOME, + text = R.string.title_home, + icon = R.drawable.ic_home_24dp, + fragment = { TimelineFragment.newInstance(TimelineViewModel.Kind.HOME) } + ) + NOTIFICATIONS -> TabData( + id = NOTIFICATIONS, + text = R.string.title_notifications, + icon = R.drawable.ic_notifications_24dp, + fragment = { NotificationsFragment.newInstance() } + ) + LOCAL -> TabData( + id = LOCAL, + text = R.string.title_public_local, + icon = R.drawable.ic_local_24dp, + fragment = { TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_LOCAL) } + ) + FEDERATED -> TabData( + id = FEDERATED, + text = R.string.title_public_federated, + icon = R.drawable.ic_public_24dp, + fragment = { TimelineFragment.newInstance(TimelineViewModel.Kind.PUBLIC_FEDERATED) } + ) + DIRECT -> TabData( + id = DIRECT, + text = R.string.title_direct_messages, + icon = R.drawable.ic_reblog_direct_24dp, + fragment = { ConversationsFragment.newInstance() } + ) + TRENDING_TAGS -> TabData( + id = TRENDING_TAGS, + text = R.string.title_public_trending_hashtags, + icon = R.drawable.ic_trending_up_24px, + fragment = { TrendingTagsFragment.newInstance() } + ) + TRENDING_STATUSES -> TabData( + id = TRENDING_STATUSES, + text = R.string.title_public_trending_statuses, + icon = R.drawable.ic_hot_24dp, + fragment = { + TimelineFragment.newInstance( + TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES + ) + } + ) + HASHTAG -> TabData( + id = HASHTAG, + text = R.string.hashtags, + icon = R.drawable.ic_hashtag, + fragment = { args -> TimelineFragment.newHashtagInstance(args) }, + arguments = arguments, + title = { context -> + arguments.joinToString(separator = " ") { + context.getString(R.string.title_tag, it) + } + } + ) + LIST -> TabData( + id = LIST, + text = R.string.list, + icon = R.drawable.ic_list, + fragment = { args -> + TimelineFragment.newInstance( + TimelineViewModel.Kind.LIST, + args.getOrNull(0).orEmpty() + ) + }, + arguments = arguments, + title = { arguments.getOrNull(1).orEmpty() } + ) + BOOKMARKS -> TabData( + id = BOOKMARKS, + text = R.string.title_bookmarks, + icon = R.drawable.ic_bookmark_active_24dp, + fragment = { TimelineFragment.newInstance(TimelineViewModel.Kind.BOOKMARKS) } + ) + else -> throw IllegalArgumentException("unknown tab type") + } +} + +fun defaultTabs(): List<TabData> { + return listOf( + createTabDataFromId(HOME), + createTabDataFromId(NOTIFICATIONS), + createTabDataFromId(LOCAL), + createTabDataFromId(DIRECT) + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt new file mode 100644 index 0000000..2fddb88 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/TabPreferenceActivity.kt @@ -0,0 +1,370 @@ +/* Copyright 2019 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky + +import android.graphics.Color +import android.os.Bundle +import android.view.View +import android.widget.FrameLayout +import androidx.activity.OnBackPressedCallback +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.AppCompatEditText +import androidx.core.view.updatePadding +import androidx.core.widget.doOnTextChanged +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.transition.TransitionManager +import at.connyduck.sparkbutton.helpers.Utils +import com.google.android.material.transition.MaterialArcMotion +import com.google.android.material.transition.MaterialContainerTransform +import com.keylesspalace.tusky.adapter.ItemInteractionListener +import com.keylesspalace.tusky.adapter.TabAdapter +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.MainTabsChangedEvent +import com.keylesspalace.tusky.components.account.list.ListSelectionFragment +import com.keylesspalace.tusky.databinding.ActivityTabPreferenceBinding +import com.keylesspalace.tusky.entity.MastoList +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.unsafeLazy +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import dagger.hilt.android.AndroidEntryPoint +import java.util.regex.Pattern +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class TabPreferenceActivity : BaseActivity(), ItemInteractionListener, ListSelectionFragment.ListSelectionListener { + + @Inject + lateinit var mastodonApi: MastodonApi + + @Inject + lateinit var eventHub: EventHub + + private val binding by viewBinding(ActivityTabPreferenceBinding::inflate) + + private lateinit var currentTabs: MutableList<TabData> + private lateinit var currentTabsAdapter: TabAdapter + private lateinit var touchHelper: ItemTouchHelper + private lateinit var addTabAdapter: TabAdapter + + private var tabsChanged = false + + private val selectedItemElevation by unsafeLazy { + resources.getDimension(R.dimen.selected_drag_item_elevation) + } + + private val hashtagRegex by unsafeLazy { + Pattern.compile("([\\w_]*[\\p{Alpha}_][\\w_]*)", Pattern.CASE_INSENSITIVE) + } + + private val onFabDismissedCallback = object : OnBackPressedCallback(false) { + override fun handleOnBackPressed() { + toggleFab(false) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) + + supportActionBar?.apply { + setTitle(R.string.title_tab_preferences) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + currentTabs = accountManager.activeAccount?.tabPreferences.orEmpty().toMutableList() + currentTabsAdapter = TabAdapter(currentTabs, false, this, currentTabs.size <= MIN_TAB_COUNT) + binding.currentTabsRecyclerView.adapter = currentTabsAdapter + binding.currentTabsRecyclerView.layoutManager = LinearLayoutManager(this) + binding.currentTabsRecyclerView.addItemDecoration( + DividerItemDecoration(this, LinearLayoutManager.VERTICAL) + ) + + addTabAdapter = TabAdapter(listOf(createTabDataFromId(DIRECT)), true, this) + binding.addTabRecyclerView.adapter = addTabAdapter + binding.addTabRecyclerView.layoutManager = LinearLayoutManager(this) + + touchHelper = ItemTouchHelper(object : ItemTouchHelper.Callback() { + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + return makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, ItemTouchHelper.END) + } + + override fun isLongPressDragEnabled(): Boolean { + return true + } + + override fun isItemViewSwipeEnabled(): Boolean { + return MIN_TAB_COUNT < currentTabs.size + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + val temp = currentTabs[viewHolder.bindingAdapterPosition] + currentTabs[viewHolder.bindingAdapterPosition] = currentTabs[target.bindingAdapterPosition] + currentTabs[target.bindingAdapterPosition] = temp + + currentTabsAdapter.notifyItemMoved(viewHolder.bindingAdapterPosition, target.bindingAdapterPosition) + saveTabs() + return true + } + + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + onTabRemoved(viewHolder.bindingAdapterPosition) + } + + override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { + if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) { + viewHolder?.itemView?.elevation = selectedItemElevation + } + } + + override fun clearView( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ) { + super.clearView(recyclerView, viewHolder) + viewHolder.itemView.elevation = 0f + } + }) + + touchHelper.attachToRecyclerView(binding.currentTabsRecyclerView) + + binding.actionButton.setOnClickListener { + toggleFab(true) + } + + binding.scrim.setOnClickListener { + toggleFab(false) + } + + updateAvailableTabs() + + onBackPressedDispatcher.addCallback(onFabDismissedCallback) + } + + override fun onTabAdded(tab: TabData) { + toggleFab(false) + + if (tab.id == HASHTAG) { + showAddHashtagDialog() + return + } + + if (tab.id == LIST) { + showSelectListDialog() + return + } + + currentTabs.add(tab) + currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) + updateAvailableTabs() + saveTabs() + } + + override fun onTabRemoved(position: Int) { + currentTabs.removeAt(position) + currentTabsAdapter.notifyItemRemoved(position) + updateAvailableTabs() + saveTabs() + } + + override fun onActionChipClicked(tab: TabData, tabPosition: Int) { + showAddHashtagDialog(tab, tabPosition) + } + + override fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int) { + val newArguments = tab.arguments.filterIndexed { i, _ -> i != chipPosition } + val newTab = tab.copy(arguments = newArguments) + currentTabs[tabPosition] = newTab + saveTabs() + + currentTabsAdapter.notifyItemChanged(tabPosition) + } + + private fun toggleFab(expand: Boolean) { + val transition = MaterialContainerTransform().apply { + startView = if (expand) binding.actionButton else binding.sheet + val endView: View = if (expand) binding.sheet else binding.actionButton + this.endView = endView + addTarget(endView) + scrimColor = Color.TRANSPARENT + setPathMotion(MaterialArcMotion()) + } + + TransitionManager.beginDelayedTransition(binding.root, transition) + binding.actionButton.visible(!expand) + binding.sheet.visible(expand) + binding.scrim.visible(expand) + + onFabDismissedCallback.isEnabled = expand + } + + private fun showAddHashtagDialog(tab: TabData? = null, tabPosition: Int = 0) { + val frameLayout = FrameLayout(this) + val padding = Utils.dpToPx(this, 8) + frameLayout.updatePadding(left = padding, right = padding) + + val editText = AppCompatEditText(this) + editText.setHint(R.string.edit_hashtag_hint) + editText.setText("") + frameLayout.addView(editText) + + val dialog = AlertDialog.Builder(this) + .setTitle(R.string.add_hashtag_title) + .setView(frameLayout) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.action_save) { _, _ -> + val input = editText.text.toString().trim() + if (tab == null) { + val newTab = createTabDataFromId(HASHTAG, listOf(input)) + currentTabs.add(newTab) + currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) + } else { + val newTab = tab.copy(arguments = tab.arguments + input) + currentTabs[tabPosition] = newTab + + currentTabsAdapter.notifyItemChanged(tabPosition) + } + + updateAvailableTabs() + saveTabs() + } + .create() + + editText.doOnTextChanged { s, _, _, _ -> + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = validateHashtag(s) + } + + dialog.show() + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = validateHashtag(editText.text) + editText.requestFocus() + } + + private var listSelectDialog: ListSelectionFragment? = null + + private fun showSelectListDialog() { + listSelectDialog = ListSelectionFragment.newInstance(null) + listSelectDialog?.show(supportFragmentManager, null) + + return + } + + override fun onListSelected(list: MastoList) { + listSelectDialog?.dismiss() + listSelectDialog = null + + val newTab = createTabDataFromId(LIST, listOf(list.id, list.title)) + currentTabs.add(newTab) + currentTabsAdapter.notifyItemInserted(currentTabs.size - 1) + updateAvailableTabs() + saveTabs() + } + + private fun validateHashtag(input: CharSequence?): Boolean { + val trimmedInput = input?.trim() ?: "" + return trimmedInput.isNotEmpty() && hashtagRegex.matcher(trimmedInput).matches() + } + + private fun updateAvailableTabs() { + val addableTabs: MutableList<TabData> = mutableListOf() + + val homeTab = createTabDataFromId(HOME) + if (!currentTabs.contains(homeTab)) { + addableTabs.add(homeTab) + } + val notificationTab = createTabDataFromId(NOTIFICATIONS) + if (!currentTabs.contains(notificationTab)) { + addableTabs.add(notificationTab) + } + val localTab = createTabDataFromId(LOCAL) + if (!currentTabs.contains(localTab)) { + addableTabs.add(localTab) + } + val federatedTab = createTabDataFromId(FEDERATED) + if (!currentTabs.contains(federatedTab)) { + addableTabs.add(federatedTab) + } + val directMessagesTab = createTabDataFromId(DIRECT) + if (!currentTabs.contains(directMessagesTab)) { + addableTabs.add(directMessagesTab) + } + val trendingTagsTab = createTabDataFromId(TRENDING_TAGS) + if (!currentTabs.contains(trendingTagsTab)) { + addableTabs.add(trendingTagsTab) + } + val bookmarksTab = createTabDataFromId(BOOKMARKS) + if (!currentTabs.contains(bookmarksTab)) { + addableTabs.add(bookmarksTab) + } + val trendingStatusesTab = createTabDataFromId(TRENDING_STATUSES) + if (!currentTabs.contains(trendingStatusesTab)) { + addableTabs.add(trendingStatusesTab) + } + + addableTabs.add(createTabDataFromId(HASHTAG)) + addableTabs.add(createTabDataFromId(LIST)) + + addTabAdapter.updateData(addableTabs) + currentTabsAdapter.setRemoveButtonVisible(currentTabs.size > MIN_TAB_COUNT) + } + + override fun onStartDelete(viewHolder: RecyclerView.ViewHolder) { + touchHelper.startSwipe(viewHolder) + } + + override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) { + touchHelper.startDrag(viewHolder) + } + + private fun saveTabs() { + accountManager.activeAccount?.let { + lifecycleScope.launch(Dispatchers.IO) { + it.tabPreferences = currentTabs + accountManager.saveAccount(it) + } + } + tabsChanged = true + } + + override fun onPause() { + super.onPause() + if (tabsChanged) { + lifecycleScope.launch { + eventHub.dispatch(MainTabsChangedEvent(currentTabs)) + } + } + } + + companion object { + private const val MIN_TAB_COUNT = 2 + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt new file mode 100644 index 0000000..26518ea --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -0,0 +1,148 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky + +import android.app.Application +import android.content.SharedPreferences +import android.util.Log +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.Configuration +import androidx.work.Constraints +import androidx.work.ExistingPeriodicWorkPolicy +import androidx.work.PeriodicWorkRequestBuilder +import androidx.work.WorkManager +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper +import com.keylesspalace.tusky.settings.AppTheme +import com.keylesspalace.tusky.settings.NEW_INSTALL_SCHEMA_VERSION +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.settings.PrefKeys.APP_THEME +import com.keylesspalace.tusky.settings.SCHEMA_VERSION +import com.keylesspalace.tusky.util.LocaleManager +import com.keylesspalace.tusky.util.setAppNightMode +import com.keylesspalace.tusky.worker.PruneCacheWorker +import dagger.hilt.android.HiltAndroidApp +import de.c1710.filemojicompat_defaults.DefaultEmojiPackList +import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper +import de.c1710.filemojicompat_ui.helpers.EmojiPreference +import java.security.Security +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import org.conscrypt.Conscrypt + +@HiltAndroidApp +class TuskyApplication : Application(), Configuration.Provider { + + @Inject + lateinit var workerFactory: HiltWorkerFactory + + @Inject + lateinit var localeManager: LocaleManager + + @Inject + lateinit var preferences: SharedPreferences + + override fun onCreate() { + // Uncomment me to get StrictMode violation logs +// if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { +// StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.Builder() +// .detectDiskReads() +// .detectDiskWrites() +// .detectNetwork() +// .detectUnbufferedIo() +// .penaltyLog() +// .build()) +// } + super.onCreate() + + Security.insertProviderAt(Conscrypt.newProvider(), 1) + + // Migrate shared preference keys and defaults from version to version. + val oldVersion = preferences.getInt( + PrefKeys.SCHEMA_VERSION, + NEW_INSTALL_SCHEMA_VERSION + ) + if (oldVersion != SCHEMA_VERSION) { + upgradeSharedPreferences(oldVersion, SCHEMA_VERSION) + } + + // In this case, we want to have the emoji preferences merged with the other ones + // Copied from PreferenceManager.getDefaultSharedPreferenceName + EmojiPreference.sharedPreferenceName = packageName + "_preferences" + EmojiPackHelper.init(this, DefaultEmojiPackList.get(this), allowPackImports = false) + + // init night mode + val theme = preferences.getString(APP_THEME, AppTheme.DEFAULT.value) + setAppNightMode(theme) + + localeManager.setLocale() + + NotificationHelper.createWorkerNotificationChannel(this) + + // Prune the database every ~ 12 hours when the device is idle. + val pruneCacheWorker = PeriodicWorkRequestBuilder<PruneCacheWorker>(12, TimeUnit.HOURS) + .setConstraints(Constraints.Builder().setRequiresDeviceIdle(true).build()) + .build() + WorkManager.getInstance(this).enqueueUniquePeriodicWork( + PruneCacheWorker.PERIODIC_WORK_TAG, + ExistingPeriodicWorkPolicy.KEEP, + pruneCacheWorker + ) + } + + override val workManagerConfiguration: Configuration + get() = Configuration.Builder() + .setWorkerFactory(workerFactory) + .build() + + private fun upgradeSharedPreferences(oldVersion: Int, newVersion: Int) { + Log.d(TAG, "Upgrading shared preferences: $oldVersion -> $newVersion") + val editor = preferences.edit() + + if (oldVersion < 2023022701) { + // These preferences are (now) handled in AccountPreferenceHandler. Remove them from shared for clarity. + + editor.remove(PrefKeys.ALWAYS_OPEN_SPOILER) + editor.remove(PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA) + editor.remove(PrefKeys.MEDIA_PREVIEW_ENABLED) + } + + if (oldVersion != NEW_INSTALL_SCHEMA_VERSION && oldVersion < 2023082301) { + // Default value for appTheme is now THEME_SYSTEM. If the user is upgrading and + // didn't have an explicit preference set use the previous default, so the + // theme does not unexpectedly change. + if (!preferences.contains(APP_THEME)) { + editor.putString(APP_THEME, AppTheme.NIGHT.value) + } + } + + if (oldVersion < 2023112001) { + editor.remove(PrefKeys.TAB_FILTER_HOME_REPLIES) + editor.remove(PrefKeys.TAB_FILTER_HOME_BOOSTS) + editor.remove(PrefKeys.TAB_SHOW_HOME_SELF_BOOSTS) + } + + if (oldVersion < 2024060201) { + editor.remove(PrefKeys.Deprecated.FAB_HIDE) + } + + editor.putInt(PrefKeys.SCHEMA_VERSION, newVersion) + editor.apply() + } + + companion object { + private const val TAG = "TuskyApplication" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt new file mode 100644 index 0000000..de142c6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/ViewMediaActivity.kt @@ -0,0 +1,405 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky + +import android.Manifest +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.app.DownloadManager +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Color +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.transition.Transition +import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.view.WindowManager +import android.webkit.MimeTypeMap +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.app.ShareCompat +import androidx.core.content.FileProvider +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.lifecycleScope +import androidx.viewpager2.adapter.FragmentStateAdapter +import androidx.viewpager2.widget.ViewPager2 +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.BuildConfig.APPLICATION_ID +import com.keylesspalace.tusky.components.viewthread.ViewThreadActivity +import com.keylesspalace.tusky.databinding.ActivityViewMediaBinding +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.fragment.ViewImageFragment +import com.keylesspalace.tusky.fragment.ViewVideoFragment +import com.keylesspalace.tusky.pager.ImagePagerAdapter +import com.keylesspalace.tusky.pager.SingleImagePagerAdapter +import com.keylesspalace.tusky.util.copyToClipboard +import com.keylesspalace.tusky.util.getParcelableArrayListExtraCompat +import com.keylesspalace.tusky.util.getTemporaryMediaFilename +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import com.keylesspalace.tusky.util.submitAsync +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import dagger.hilt.android.AndroidEntryPoint +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.util.Locale +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.launch + +typealias ToolbarVisibilityListener = (isVisible: Boolean) -> Unit + +@AndroidEntryPoint +class ViewMediaActivity : + BaseActivity(), + ViewImageFragment.PhotoActionsListener, + ViewVideoFragment.VideoActionsListener { + + private val binding by viewBinding(ActivityViewMediaBinding::inflate) + + val toolbar: View + get() = binding.toolbar + + var isToolbarVisible = true + private set + + private var attachments: ArrayList<AttachmentViewData>? = null + private val toolbarVisibilityListeners = mutableListOf<ToolbarVisibilityListener>() + private var imageUrl: String? = null + + private val requestDownloadMediaPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + downloadMedia() + } else { + showErrorDialog( + binding.toolbar, + R.string.error_media_download_permission, + R.string.action_retry + ) { requestDownloadMedia() } + } + } + + fun addToolbarVisibilityListener(listener: ToolbarVisibilityListener): Function0<Boolean> { + this.toolbarVisibilityListeners.add(listener) + listener(isToolbarVisible) + return { toolbarVisibilityListeners.remove(listener) } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + supportPostponeEnterTransition() + + // Gather the parameters. + attachments = intent.getParcelableArrayListExtraCompat(EXTRA_ATTACHMENTS) + val initialPosition = intent.getIntExtra(EXTRA_ATTACHMENT_INDEX, 0) + + // Adapter is actually of existential type PageAdapter & SharedElementsTransitionListener + // but it cannot be expressed and if I don't specify type explicitly compilation fails + // (probably a bug in compiler) + val adapter: ViewMediaAdapter = if (attachments != null) { + val realAttachs = attachments!!.map(AttachmentViewData::attachment) + // Setup the view pager. + ImagePagerAdapter(this, realAttachs, initialPosition) + } else { + imageUrl = intent.getStringExtra(EXTRA_SINGLE_IMAGE_URL) + ?: throw IllegalArgumentException("attachment list or image url has to be set") + + SingleImagePagerAdapter(this, imageUrl!!) + } + + binding.viewPager.adapter = adapter + binding.viewPager.setCurrentItem(initialPosition, false) + binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + binding.toolbar.title = getPageTitle(position) + adjustScreenWakefulness() + } + }) + + // Setup the toolbar. + setSupportActionBar(binding.toolbar) + val actionBar = supportActionBar + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true) + actionBar.setDisplayShowHomeEnabled(true) + actionBar.title = getPageTitle(initialPosition) + } + binding.toolbar.setNavigationOnClickListener { supportFinishAfterTransition() } + binding.toolbar.setOnMenuItemClickListener { item: MenuItem -> + when (item.itemId) { + R.id.action_download -> requestDownloadMedia() + R.id.action_open_status -> onOpenStatus() + R.id.action_share_media -> shareMedia() + R.id.action_copy_media_link -> copyLink() + } + true + } + + window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LOW_PROFILE + + window.statusBarColor = Color.BLACK + window.sharedElementEnterTransition.addListener(object : NoopTransitionListener { + override fun onTransitionEnd(transition: Transition) { + adapter.onTransitionEnd(binding.viewPager.currentItem) + window.sharedElementEnterTransition.removeListener(this) + } + }) + + adjustScreenWakefulness() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.view_media_toolbar, menu) + // We don't support 'open status' from single image views + menu.findItem(R.id.action_open_status)?.isVisible = (attachments != null) + return true + } + + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + menu?.findItem(R.id.action_share_media)?.isEnabled = !isCreating + return true + } + + override fun onBringUp() { + supportStartPostponedEnterTransition() + } + + override fun onDismiss() { + supportFinishAfterTransition() + } + + override fun onPhotoTap() { + isToolbarVisible = !isToolbarVisible + for (listener in toolbarVisibilityListeners) { + listener(isToolbarVisible) + } + + val visibility = if (isToolbarVisible) View.VISIBLE else View.INVISIBLE + val alpha = if (isToolbarVisible) 1.0f else 0.0f + if (isToolbarVisible) { + // If to be visible, need to make visible immediately and animate alpha + binding.toolbar.alpha = 0.0f + binding.toolbar.visibility = visibility + } + + binding.toolbar.animate().alpha(alpha) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + binding.toolbar.visibility = visibility + animation.removeListener(this) + } + }) + .start() + } + + private fun getPageTitle(position: Int): CharSequence { + if (attachments == null) { + return "" + } + return String.format(Locale.getDefault(), "%d/%d", position + 1, attachments?.size) + } + + private fun downloadMedia() { + val url = imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url + val filename = Uri.parse(url).lastPathSegment + Toast.makeText( + applicationContext, + resources.getString(R.string.download_image, filename), + Toast.LENGTH_SHORT + ).show() + + val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + val request = DownloadManager.Request(Uri.parse(url)) + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename) + downloadManager.enqueue(request) + } + + private fun requestDownloadMedia() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + requestDownloadMediaPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } else { + downloadMedia() + } + } + + private fun onOpenStatus() { + val attach = attachments!![binding.viewPager.currentItem] + startActivityWithSlideInAnimation( + ViewThreadActivity.startIntent(this, attach.statusId, attach.statusUrl) + ) + } + + private fun copyLink() { + copyToClipboard( + imageUrl ?: attachments!![binding.viewPager.currentItem].attachment.url, + getString(R.string.url_copied), + ) + } + + private fun shareMedia() { + val directory = applicationContext.getExternalFilesDir("Tusky") + if (directory == null || !(directory.exists())) { + Log.e(TAG, "Error obtaining directory to save temporary media.") + return + } + + if (imageUrl != null) { + shareImage(directory, imageUrl!!) + } else { + val attachment = attachments!![binding.viewPager.currentItem].attachment + when (attachment.type) { + Attachment.Type.IMAGE -> shareImage(directory, attachment.url) + Attachment.Type.AUDIO, + Attachment.Type.VIDEO, + Attachment.Type.GIFV -> shareMediaFile(directory, attachment.url) + else -> Log.e(TAG, "Unknown media format for sharing.") + } + } + } + + private fun shareFile(file: File, mimeType: String?) { + ShareCompat.IntentBuilder(this) + .setType(mimeType) + .addStream( + FileProvider.getUriForFile(applicationContext, "$APPLICATION_ID.fileprovider", file) + ) + .setChooserTitle(R.string.send_media_to) + .startChooser() + } + + private var isCreating: Boolean = false + + private fun shareImage(directory: File, url: String) { + isCreating = true + binding.progressBarShare.visibility = View.VISIBLE + invalidateOptionsMenu() + + lifecycleScope.launch { + val file = File(directory, getTemporaryMediaFilename("png")) + val result = try { + val bitmap = + Glide.with(applicationContext).asBitmap().load(Uri.parse(url)).submitAsync() + try { + FileOutputStream(file).use { stream -> + bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream) + } + true + } catch (ioe: IOException) { + // FileNotFoundException is covered by IOException + Log.e(TAG, "Error writing temporary media.") + false + }.also { result -> Log.d(TAG, "Download image result: $result") } + } catch (error: Throwable) { + if (error is CancellationException) { + throw error + } + Log.e(TAG, "Failed to download image", error) + false + } + + isCreating = false + invalidateOptionsMenu() + binding.progressBarShare.visibility = View.GONE + if (result) { + shareFile(file, "image/png") + } + } + } + + private fun shareMediaFile(directory: File, url: String) { + val uri = Uri.parse(url) + val mimeTypeMap = MimeTypeMap.getSingleton() + val extension = MimeTypeMap.getFileExtensionFromUrl(url) + val mimeType = mimeTypeMap.getMimeTypeFromExtension(extension) + val filename = getTemporaryMediaFilename(extension) + val file = File(directory, filename) + + val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + val request = DownloadManager.Request(uri) + request.setDestinationUri(Uri.fromFile(file)) + request.setVisibleInDownloadsUi(false) + downloadManager.enqueue(request) + + shareFile(file, mimeType) + } + + // Prevent this activity from dimming or sleeping the screen if, and only if, it is playing video or audio + private fun adjustScreenWakefulness() { + attachments?.run { + if (get(binding.viewPager.currentItem).attachment.type == Attachment.Type.IMAGE) { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + } + + companion object { + private const val EXTRA_ATTACHMENTS = "attachments" + private const val EXTRA_ATTACHMENT_INDEX = "index" + private const val EXTRA_SINGLE_IMAGE_URL = "single_image" + private const val TAG = "ViewMediaActivity" + + @JvmStatic + fun newIntent( + context: Context?, + attachments: List<AttachmentViewData>, + index: Int + ): Intent { + val intent = Intent(context, ViewMediaActivity::class.java) + intent.putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments)) + intent.putExtra(EXTRA_ATTACHMENT_INDEX, index) + return intent + } + + @JvmStatic + fun newSingleImageIntent(context: Context, url: String): Intent { + val intent = Intent(context, ViewMediaActivity::class.java) + intent.putExtra(EXTRA_SINGLE_IMAGE_URL, url) + return intent + } + } +} + +abstract class ViewMediaAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) { + abstract fun onTransitionEnd(position: Int) +} + +interface NoopTransitionListener : Transition.TransitionListener { + override fun onTransitionEnd(transition: Transition) { + } + + override fun onTransitionResume(transition: Transition) { + } + + override fun onTransitionPause(transition: Transition) { + } + + override fun onTransitionCancel(transition: Transition) { + } + + override fun onTransitionStart(transition: Transition) { + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt new file mode 100644 index 0000000..890e956 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountFieldEditAdapter.kt @@ -0,0 +1,109 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.widget.doAfterTextChanged +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.databinding.ItemEditFieldBinding +import com.keylesspalace.tusky.entity.StringField +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.fixTextSelection + +class AccountFieldEditAdapter( + var onFieldsChanged: () -> Unit = { } +) : RecyclerView.Adapter<BindingHolder<ItemEditFieldBinding>>() { + + private val fieldData = mutableListOf<MutableStringPair>() + private var maxNameLength: Int? = null + private var maxValueLength: Int? = null + + fun setFields(fields: List<StringField>) { + fieldData.clear() + + fields.forEach { field -> + fieldData.add(MutableStringPair(field.name, field.value)) + } + if (fieldData.isEmpty()) { + fieldData.add(MutableStringPair("", "")) + } + + notifyDataSetChanged() + } + + fun setFieldLimits(maxNameLength: Int?, maxValueLength: Int?) { + this.maxNameLength = maxNameLength + this.maxValueLength = maxValueLength + notifyDataSetChanged() + } + + fun getFieldData(): List<StringField> { + return fieldData.map { + StringField(it.first, it.second) + } + } + + fun addField() { + fieldData.add(MutableStringPair("", "")) + notifyItemInserted(fieldData.size - 1) + } + + override fun getItemCount() = fieldData.size + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemEditFieldBinding> { + val binding = ItemEditFieldBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return BindingHolder(binding) + } + + override fun onBindViewHolder(holder: BindingHolder<ItemEditFieldBinding>, position: Int) { + holder.binding.accountFieldNameText.setText(fieldData[position].first) + holder.binding.accountFieldValueText.setText(fieldData[position].second) + + holder.binding.accountFieldNameTextLayout.isCounterEnabled = maxNameLength != null + maxNameLength?.let { + holder.binding.accountFieldNameTextLayout.counterMaxLength = it + } + + holder.binding.accountFieldValueTextLayout.isCounterEnabled = maxValueLength != null + maxValueLength?.let { + holder.binding.accountFieldValueTextLayout.counterMaxLength = it + } + + holder.binding.accountFieldNameText.doAfterTextChanged { newText -> + fieldData.getOrNull(holder.bindingAdapterPosition)?.first = newText.toString() + onFieldsChanged() + } + + holder.binding.accountFieldValueText.doAfterTextChanged { newText -> + fieldData.getOrNull(holder.bindingAdapterPosition)?.second = newText.toString() + onFieldsChanged() + } + + // Ensure the textview contents are selectable + holder.binding.accountFieldNameText.fixTextSelection() + holder.binding.accountFieldValueText.fixTextSelection() + } + + class MutableStringPair(var first: String, var second: String) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt new file mode 100644 index 0000000..8d42f0a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountSelectionAdapter.kt @@ -0,0 +1,58 @@ +/* Copyright 2019 Levi Bard + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemAutocompleteAccountBinding +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.loadAvatar + +class AccountSelectionAdapter( + context: Context, + private val animateAvatars: Boolean, + private val animateEmojis: Boolean +) : ArrayAdapter<AccountEntity>( + context, + R.layout.item_autocomplete_account +) { + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val binding = if (convertView == null) { + ItemAutocompleteAccountBinding.inflate(LayoutInflater.from(context), parent, false) + } else { + ItemAutocompleteAccountBinding.bind(convertView) + } + + val account = getItem(position) + if (account != null) { + binding.username.text = account.fullName + binding.displayName.text = account.displayName.emojify(account.emojis, binding.displayName, animateEmojis) + binding.avatarBadge.visibility = View.GONE // We never want to display the bot badge here + + val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_42dp) + + loadAvatar(account.profilePictureUrl, binding.avatar, avatarRadius, animateAvatars) + } + + return binding.root + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.kt new file mode 100644 index 0000000..f125422 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/AccountViewHolder.kt @@ -0,0 +1,56 @@ +package com.keylesspalace.tusky.adapter + +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemAccountBinding +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.visible + +class AccountViewHolder( + private val binding: ItemAccountBinding +) : RecyclerView.ViewHolder(binding.root) { + private lateinit var accountId: String + + fun setupWithAccount( + account: TimelineAccount, + animateAvatar: Boolean, + animateEmojis: Boolean, + showBotOverlay: Boolean + ) { + accountId = account.id + + binding.accountUsername.text = binding.accountUsername.context.getString( + R.string.post_username_format, + account.username + ) + + val emojifiedName = account.name.emojify( + account.emojis, + binding.accountDisplayName, + animateEmojis + ) + binding.accountDisplayName.text = emojifiedName + + val avatarRadius = binding.accountAvatar.context.resources + .getDimensionPixelSize(R.dimen.avatar_radius_48dp) + loadAvatar(account.avatar, binding.accountAvatar, avatarRadius, animateAvatar) + + binding.accountBotBadge.visible(showBotOverlay && account.bot) + } + + fun setupActionListener(listener: AccountActionListener) { + itemView.setOnClickListener { listener.onViewAccount(accountId) } + } + + fun setupLinkListener(listener: LinkListener) { + itemView.setOnClickListener { + listener.onViewAccount( + accountId + ) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt new file mode 100644 index 0000000..f2162fa --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/EmojiAdapter.kt @@ -0,0 +1,77 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.appcompat.widget.TooltipCompat +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.databinding.ItemEmojiButtonBinding +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.util.BindingHolder +import java.util.Locale + +class EmojiAdapter( + emojiList: List<Emoji>, + private val onEmojiSelectedListener: OnEmojiSelectedListener, + private val animate: Boolean +) : RecyclerView.Adapter<BindingHolder<ItemEmojiButtonBinding>>() { + + private val emojiList: List<Emoji> = emojiList.filter { emoji -> emoji.visibleInPicker } + .sortedBy { it.shortcode.lowercase(Locale.ROOT) } + + override fun getItemCount() = emojiList.size + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemEmojiButtonBinding> { + val binding = ItemEmojiButtonBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return BindingHolder(binding) + } + + override fun onBindViewHolder(holder: BindingHolder<ItemEmojiButtonBinding>, position: Int) { + val emoji = emojiList[position] + val emojiImageView = holder.binding.root + + if (animate) { + Glide.with(emojiImageView) + .load(emoji.url) + .into(emojiImageView) + } else { + Glide.with(emojiImageView) + .asBitmap() + .load(emoji.url) + .into(emojiImageView) + } + + emojiImageView.setOnClickListener { + onEmojiSelectedListener.onEmojiSelected(emoji.shortcode) + } + + emojiImageView.contentDescription = emoji.shortcode + TooltipCompat.setTooltipText(emojiImageView, emoji.shortcode) + } +} + +interface OnEmojiSelectedListener { + fun onEmojiSelected(shortcode: String) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt new file mode 100644 index 0000000..5801771 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/FollowRequestViewHolder.kt @@ -0,0 +1,120 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.adapter + +import android.graphics.Typeface +import android.text.SpannableString +import android.text.Spanned +import android.text.style.StyleSpan +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.notifications.NotificationsViewHolder +import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.parseAsMastodonHtml +import com.keylesspalace.tusky.util.setClickableText +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.unicodeWrap +import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.viewdata.NotificationViewData + +class FollowRequestViewHolder( + private val binding: ItemFollowRequestBinding, + private val accountListener: AccountActionListener, + private val linkListener: LinkListener, + private val showHeader: Boolean +) : RecyclerView.ViewHolder(binding.root), NotificationsViewHolder { + + override fun bind( + viewData: NotificationViewData.Concrete, + payloads: List<*>, + statusDisplayOptions: StatusDisplayOptions + ) { + setupWithAccount( + viewData.account, + statusDisplayOptions.animateAvatars, + statusDisplayOptions.animateEmojis, + statusDisplayOptions.showBotOverlay + ) + setupActionListener(accountListener, viewData.account.id) + } + + fun setupWithAccount( + account: TimelineAccount, + animateAvatar: Boolean, + animateEmojis: Boolean, + showBotOverlay: Boolean + ) { + val wrappedName = account.name.unicodeWrap() + val emojifiedName: CharSequence = wrappedName.emojify( + account.emojis, + binding.displayNameTextView, + animateEmojis + ) + binding.displayNameTextView.text = emojifiedName + if (showHeader) { + val wholeMessage: String = itemView.context.getString( + R.string.notification_follow_request_format, + wrappedName + ) + binding.notificationTextView.text = SpannableString(wholeMessage).apply { + setSpan(StyleSpan(Typeface.BOLD), 0, wrappedName.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) + }.emojify(account.emojis, binding.notificationTextView, animateEmojis) + } + binding.notificationTextView.visible(showHeader) + val formattedUsername = itemView.context.getString( + R.string.post_username_format, + account.username + ) + binding.usernameTextView.text = formattedUsername + if (account.note.isEmpty()) { + binding.accountNote.hide() + } else { + binding.accountNote.show() + + val emojifiedNote = account.note.parseAsMastodonHtml() + .emojify(account.emojis, binding.accountNote, animateEmojis) + setClickableText(binding.accountNote, emojifiedNote, emptyList(), null, linkListener) + } + val avatarRadius = binding.avatar.context.resources.getDimensionPixelSize( + R.dimen.avatar_radius_48dp + ) + loadAvatar(account.avatar, binding.avatar, avatarRadius, animateAvatar) + binding.avatarBadge.visible(showBotOverlay && account.bot) + } + + fun setupActionListener(listener: AccountActionListener, accountId: String) { + binding.acceptButton.setOnClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + listener.onRespondToFollowRequest(true, accountId, position) + } + } + binding.rejectButton.setOnClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + listener.onRespondToFollowRequest(false, accountId, position) + } + } + itemView.setOnClickListener { listener.onViewAccount(accountId) } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt new file mode 100644 index 0000000..a780356 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/LocaleAdapter.kt @@ -0,0 +1,48 @@ +/* Copyright 2022 Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.adapter + +import android.content.Context +import android.graphics.Typeface +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.TextView +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.util.getTuskyDisplayName +import com.keylesspalace.tusky.util.modernLanguageCode +import java.util.Locale + +class LocaleAdapter(context: Context, resource: Int, locales: List<Locale>) : ArrayAdapter<Locale>( + context, + resource, + locales +) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + return (super.getView(position, convertView, parent) as TextView).apply { + setTextColor(MaterialColors.getColor(this, android.R.attr.textColorTertiary)) + typeface = Typeface.DEFAULT_BOLD + text = super.getItem(position)?.modernLanguageCode?.uppercase() + } + } + + override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View { + return (super.getDropDownView(position, convertView, parent) as TextView).apply { + setTextColor(MaterialColors.getColor(this, android.R.attr.textColorTertiary)) + text = super.getItem(position)?.getTuskyDisplayName(context) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt new file mode 100644 index 0000000..88dead0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PlaceholderViewHolder.kt @@ -0,0 +1,47 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ +package com.keylesspalace.tusky.adapter + +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.visible + +/** + * Placeholder for missing parts in timelines. + * + * Displays a "Load more" button to load the gap, or a + * circular progress bar if the missing page is being loaded. + */ +class PlaceholderViewHolder( + private val binding: ItemStatusPlaceholderBinding, + listener: StatusActionListener +) : RecyclerView.ViewHolder(binding.root) { + + init { + binding.loadMoreButton.setOnClickListener { + binding.loadMoreButton.hide() + binding.loadMoreProgressBar.show() + listener.onLoadMore(bindingAdapterPosition) + } + } + + fun setup(loading: Boolean) { + binding.loadMoreButton.visible(!loading) + binding.loadMoreProgressBar.visible(loading) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt new file mode 100644 index 0000000..aac3c81 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PollAdapter.kt @@ -0,0 +1,136 @@ +/* Copyright 2019 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemPollBinding +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.viewdata.PollOptionViewData +import com.keylesspalace.tusky.viewdata.buildDescription +import com.keylesspalace.tusky.viewdata.calculatePercent + +class PollAdapter : RecyclerView.Adapter<BindingHolder<ItemPollBinding>>() { + + private var pollOptions: List<PollOptionViewData> = emptyList() + private var voteCount: Int = 0 + private var votersCount: Int? = null + private var mode = RESULT + private var emojis: List<Emoji> = emptyList() + private var resultClickListener: View.OnClickListener? = null + private var animateEmojis = false + private var enabled = true + + @JvmOverloads + fun setup( + options: List<PollOptionViewData>, + voteCount: Int, + votersCount: Int?, + emojis: List<Emoji>, + mode: Int, + resultClickListener: View.OnClickListener?, + animateEmojis: Boolean, + enabled: Boolean = true + ) { + this.pollOptions = options + this.voteCount = voteCount + this.votersCount = votersCount + this.emojis = emojis + this.mode = mode + this.resultClickListener = resultClickListener + this.animateEmojis = animateEmojis + this.enabled = enabled + notifyDataSetChanged() + } + + fun getSelected(): List<Int> { + return pollOptions.filter { it.selected } + .map { pollOptions.indexOf(it) } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemPollBinding> { + val binding = ItemPollBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) + } + + override fun getItemCount() = pollOptions.size + + override fun onBindViewHolder(holder: BindingHolder<ItemPollBinding>, position: Int) { + val option = pollOptions[position] + + val resultTextView = holder.binding.statusPollOptionResult + val radioButton = holder.binding.statusPollRadioButton + val checkBox = holder.binding.statusPollCheckbox + + resultTextView.visible(mode == RESULT) + radioButton.visible(mode == SINGLE) + checkBox.visible(mode == MULTIPLE) + + radioButton.isEnabled = enabled + checkBox.isEnabled = enabled + + when (mode) { + RESULT -> { + val percent = calculatePercent(option.votesCount, votersCount, voteCount) + resultTextView.text = buildDescription(option.title, percent, option.voted, resultTextView.context) + .emojify(emojis, resultTextView, animateEmojis) + + val level = percent * 100 + val optionColor = if (option.voted) { + R.color.colorBackgroundHighlight + } else { + R.color.colorBackgroundAccent + } + + resultTextView.background.level = level + resultTextView.background.setTint(resultTextView.context.getColor(optionColor)) + resultTextView.setOnClickListener(resultClickListener) + } + SINGLE -> { + radioButton.text = option.title.emojify(emojis, radioButton, animateEmojis) + radioButton.isChecked = option.selected + radioButton.setOnClickListener { + pollOptions.forEachIndexed { index, pollOption -> + pollOption.selected = index == holder.bindingAdapterPosition + notifyItemChanged(index) + } + } + } + MULTIPLE -> { + checkBox.text = option.title.emojify(emojis, checkBox, animateEmojis) + checkBox.isChecked = option.selected + checkBox.setOnCheckedChangeListener { _, isChecked -> + pollOptions[holder.bindingAdapterPosition].selected = isChecked + } + } + } + } + + companion object { + const val RESULT = 0 + const val SINGLE = 1 + const val MULTIPLE = 2 + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt new file mode 100644 index 0000000..10c2f14 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/PreviewPollOptionsAdapter.kt @@ -0,0 +1,69 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.adapter + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.core.widget.TextViewCompat +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R + +class PreviewPollOptionsAdapter : RecyclerView.Adapter<PreviewViewHolder>() { + + private var options: List<String> = emptyList() + private var multiple: Boolean = false + private var clickListener: View.OnClickListener? = null + + fun update(newOptions: List<String>, multiple: Boolean) { + this.options = newOptions + this.multiple = multiple + notifyDataSetChanged() + } + + fun setOnClickListener(l: View.OnClickListener?) { + clickListener = l + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PreviewViewHolder { + return PreviewViewHolder( + LayoutInflater.from( + parent.context + ).inflate(R.layout.item_poll_preview_option, parent, false) + ) + } + + override fun getItemCount() = options.size + + override fun onBindViewHolder(holder: PreviewViewHolder, position: Int) { + val textView = holder.itemView as TextView + + val iconId = if (multiple) { + R.drawable.ic_check_box_outline_blank_18dp + } else { + R.drawable.ic_radio_button_unchecked_18dp + } + + TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(textView, iconId, 0, 0, 0) + + textView.text = options[position] + + textView.setOnClickListener(clickListener) + } +} + +class PreviewViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java new file mode 100644 index 0000000..0cf9cb1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusBaseViewHolder.java @@ -0,0 +1,1318 @@ +package com.keylesspalace.tusky.adapter; + +import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; + +import android.content.Context; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.format.DateUtils; +import android.view.Gravity; +import android.view.Menu; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.appcompat.widget.PopupMenu; +import androidx.appcompat.widget.TooltipCompat; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.text.HtmlCompat; +import androidx.recyclerview.widget.DefaultItemAnimator; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.RequestBuilder; +import com.google.android.material.button.MaterialButton; +import com.google.android.material.color.MaterialColors; +import com.google.android.material.imageview.ShapeableImageView; +import com.google.android.material.shape.CornerFamily; +import com.google.android.material.shape.ShapeAppearanceModel; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.ViewMediaActivity; +import com.keylesspalace.tusky.entity.Attachment; +import com.keylesspalace.tusky.entity.Attachment.Focus; +import com.keylesspalace.tusky.entity.Attachment.MetaData; +import com.keylesspalace.tusky.entity.Card; +import com.keylesspalace.tusky.entity.Emoji; +import com.keylesspalace.tusky.entity.Filter; +import com.keylesspalace.tusky.entity.FilterResult; +import com.keylesspalace.tusky.entity.HashTag; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.entity.Translation; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.AbsoluteTimeFormatter; +import com.keylesspalace.tusky.util.AttachmentHelper; +import com.keylesspalace.tusky.util.CardViewMode; +import com.keylesspalace.tusky.util.CompositeWithOpaqueBackground; +import com.keylesspalace.tusky.util.CustomEmojiHelper; +import com.keylesspalace.tusky.util.ImageLoadingHelper; +import com.keylesspalace.tusky.util.LinkHelper; +import com.keylesspalace.tusky.util.LocaleUtilsKt; +import com.keylesspalace.tusky.util.NumberUtils; +import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.util.TimestampUtils; +import com.keylesspalace.tusky.util.TouchDelegateHelper; +import com.keylesspalace.tusky.view.MediaPreviewImageView; +import com.keylesspalace.tusky.view.MediaPreviewLayout; +import com.keylesspalace.tusky.viewdata.PollOptionViewData; +import com.keylesspalace.tusky.viewdata.PollViewData; +import com.keylesspalace.tusky.viewdata.PollViewDataKt; +import com.keylesspalace.tusky.viewdata.StatusViewData; +import com.keylesspalace.tusky.viewdata.TranslationViewData; + +import java.text.NumberFormat; +import java.util.Collections; +import java.util.Date; +import java.util.List; + +import at.connyduck.sparkbutton.SparkButton; +import at.connyduck.sparkbutton.helpers.Utils; +import kotlin.collections.CollectionsKt; + +public abstract class StatusBaseViewHolder extends RecyclerView.ViewHolder { + public static class Key { + public static final String KEY_CREATED = "created"; + } + + private final String TAG = "StatusBaseViewHolder"; + + private final TextView displayName; + private final TextView username; + private final ImageButton replyButton; + private final TextView replyCountLabel; + private final SparkButton reblogButton; + private final SparkButton favouriteButton; + private final SparkButton bookmarkButton; + private final ImageButton moreButton; + private final ConstraintLayout mediaContainer; + protected final MediaPreviewLayout mediaPreview; + private final TextView sensitiveMediaWarning; + private final View sensitiveMediaShow; + protected final TextView[] mediaLabels; + protected final CharSequence[] mediaDescriptions; + private final MaterialButton contentWarningButton; + private final ImageView avatarInset; + + public final ImageView avatar; + public final TextView metaInfo; + public final TextView content; + public final TextView contentWarningDescription; + + private final RecyclerView pollOptions; + private final TextView pollDescription; + private final Button pollButton; + + private final LinearLayout cardView; + private final LinearLayout cardInfo; + private final ShapeableImageView cardImage; + private final TextView cardTitle; + private final TextView cardDescription; + private final TextView cardUrl; + private final PollAdapter pollAdapter; + protected final LinearLayout filteredPlaceholder; + protected final TextView filteredPlaceholderLabel; + protected final Button filteredPlaceholderShowButton; + protected final ConstraintLayout statusContainer; + private final TextView translationStatusView; + private final Button untranslateButton; + + + private final NumberFormat numberFormat = NumberFormat.getNumberInstance(); + private final AbsoluteTimeFormatter absoluteTimeFormatter = new AbsoluteTimeFormatter(); + + protected final int avatarRadius48dp; + private final int avatarRadius36dp; + private final int avatarRadius24dp; + + private final Drawable mediaPreviewUnloaded; + + protected StatusBaseViewHolder(@NonNull View itemView) { + super(itemView); + displayName = itemView.findViewById(R.id.status_display_name); + username = itemView.findViewById(R.id.status_username); + metaInfo = itemView.findViewById(R.id.status_meta_info); + content = itemView.findViewById(R.id.status_content); + avatar = itemView.findViewById(R.id.status_avatar); + replyButton = itemView.findViewById(R.id.status_reply); + replyCountLabel = itemView.findViewById(R.id.status_replies); + reblogButton = itemView.findViewById(R.id.status_inset); + favouriteButton = itemView.findViewById(R.id.status_favourite); + bookmarkButton = itemView.findViewById(R.id.status_bookmark); + moreButton = itemView.findViewById(R.id.status_more); + + mediaContainer = itemView.findViewById(R.id.status_media_preview_container); + mediaContainer.setClipToOutline(true); + mediaPreview = itemView.findViewById(R.id.status_media_preview); + + sensitiveMediaWarning = itemView.findViewById(R.id.status_sensitive_media_warning); + sensitiveMediaShow = itemView.findViewById(R.id.status_sensitive_media_button); + mediaLabels = new TextView[]{ + itemView.findViewById(R.id.status_media_label_0), + itemView.findViewById(R.id.status_media_label_1), + itemView.findViewById(R.id.status_media_label_2), + itemView.findViewById(R.id.status_media_label_3) + }; + mediaDescriptions = new CharSequence[mediaLabels.length]; + contentWarningDescription = itemView.findViewById(R.id.status_content_warning_description); + contentWarningButton = itemView.findViewById(R.id.status_content_warning_button); + avatarInset = itemView.findViewById(R.id.status_avatar_inset); + + pollOptions = itemView.findViewById(R.id.status_poll_options); + pollDescription = itemView.findViewById(R.id.status_poll_description); + pollButton = itemView.findViewById(R.id.status_poll_button); + + cardView = itemView.findViewById(R.id.status_card_view); + cardInfo = itemView.findViewById(R.id.card_info); + cardImage = itemView.findViewById(R.id.card_image); + cardTitle = itemView.findViewById(R.id.card_title); + cardDescription = itemView.findViewById(R.id.card_description); + cardUrl = itemView.findViewById(R.id.card_link); + + filteredPlaceholder = itemView.findViewById(R.id.status_filtered_placeholder); + filteredPlaceholderLabel = itemView.findViewById(R.id.status_filter_label); + filteredPlaceholderShowButton = itemView.findViewById(R.id.status_filter_show_anyway); + statusContainer = itemView.findViewById(R.id.status_container); + + pollAdapter = new PollAdapter(); + pollOptions.setAdapter(pollAdapter); + pollOptions.setLayoutManager(new LinearLayoutManager(pollOptions.getContext())); + ((DefaultItemAnimator) pollOptions.getItemAnimator()).setSupportsChangeAnimations(false); + + translationStatusView = itemView.findViewById(R.id.status_translation_status); + untranslateButton = itemView.findViewById(R.id.status_button_untranslate); + + this.avatarRadius48dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_48dp); + this.avatarRadius36dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_36dp); + this.avatarRadius24dp = itemView.getContext().getResources().getDimensionPixelSize(R.dimen.avatar_radius_24dp); + + mediaPreviewUnloaded = new ColorDrawable(MaterialColors.getColor(itemView, R.attr.colorBackgroundAccent)); + + TouchDelegateHelper.expandTouchSizeToFillRow((ViewGroup) itemView, CollectionsKt.listOfNotNull(replyButton, reblogButton, favouriteButton, bookmarkButton, moreButton)); + } + + protected void setDisplayName(@NonNull String name, @NonNull List<Emoji> customEmojis, @NonNull StatusDisplayOptions statusDisplayOptions) { + CharSequence emojifiedName = CustomEmojiHelper.emojify( + name, customEmojis, displayName, statusDisplayOptions.animateEmojis() + ); + displayName.setText(emojifiedName); + } + + protected void setUsername(@Nullable String name) { + Context context = username.getContext(); + String usernameText = context.getString(R.string.post_username_format, name); + username.setText(usernameText); + } + + public void toggleContentWarning() { + contentWarningButton.performClick(); + } + + protected void setSpoilerAndContent(@NonNull StatusViewData.Concrete status, + @NonNull StatusDisplayOptions statusDisplayOptions, + final @NonNull StatusActionListener listener) { + + Status actionable = status.getActionable(); + String spoilerText = status.getSpoilerText(); + List<Emoji> emojis = actionable.getEmojis(); + + boolean sensitive = !TextUtils.isEmpty(spoilerText); + boolean expanded = status.isExpanded(); + + if (sensitive) { + CharSequence emojiSpoiler = CustomEmojiHelper.emojify( + spoilerText, emojis, contentWarningDescription, statusDisplayOptions.animateEmojis() + ); + contentWarningDescription.setText(emojiSpoiler); + contentWarningDescription.setVisibility(View.VISIBLE); + boolean hasContent = !TextUtils.isEmpty(status.getContent()); + if (hasContent) { + contentWarningButton.setVisibility(View.VISIBLE); + setContentWarningButtonText(expanded); + contentWarningButton.setOnClickListener(view -> toggleExpandedState(true, !expanded, status, statusDisplayOptions, listener)); + } else { + contentWarningButton.setVisibility(View.GONE); + } + this.setTextVisible(true, expanded, status, statusDisplayOptions, listener); + } else { + contentWarningDescription.setVisibility(View.GONE); + contentWarningButton.setVisibility(View.GONE); + this.setTextVisible(false, true, status, statusDisplayOptions, listener); + } + } + + private void setContentWarningButtonText(boolean expanded) { + if (expanded) { + contentWarningButton.setText(R.string.post_content_warning_show_less); + } else { + contentWarningButton.setText(R.string.post_content_warning_show_more); + } + } + + protected void toggleExpandedState(boolean sensitive, + boolean expanded, + @NonNull final StatusViewData.Concrete status, + @NonNull final StatusDisplayOptions statusDisplayOptions, + @NonNull final StatusActionListener listener) { + + contentWarningDescription.invalidate(); + int adapterPosition = getBindingAdapterPosition(); + if (adapterPosition != RecyclerView.NO_POSITION) { + listener.onExpandedChange(expanded, adapterPosition); + } + setContentWarningButtonText(expanded); + + this.setTextVisible(sensitive, expanded, status, statusDisplayOptions, listener); + + setupCard(status, expanded, statusDisplayOptions.cardViewMode(), statusDisplayOptions, listener); + } + + private void setTextVisible(boolean sensitive, + boolean expanded, + @NonNull final StatusViewData.Concrete status, + @NonNull final StatusDisplayOptions statusDisplayOptions, + final StatusActionListener listener) { + + Status actionable = status.getActionable(); + Spanned content = status.getContent(); + List<Status.Mention> mentions = actionable.getMentions(); + List<HashTag> tags = actionable.getTags(); + List<Emoji> emojis = actionable.getEmojis(); + PollViewData poll = PollViewDataKt.toViewData(status.getPoll()); + + if (expanded) { + CharSequence emojifiedText = CustomEmojiHelper.emojify(content, emojis, this.content, statusDisplayOptions.animateEmojis()); + LinkHelper.setClickableText(this.content, emojifiedText, mentions, tags, listener); + for (int i = 0; i < mediaLabels.length; ++i) { + updateMediaLabel(i, sensitive, true); + } + if (poll != null) { + setupPoll(poll, emojis, statusDisplayOptions, listener); + } else { + hidePoll(); + } + } else { + hidePoll(); + LinkHelper.setClickableMentions(this.content, mentions, listener); + } + if (TextUtils.isEmpty(this.content.getText())) { + this.content.setVisibility(View.GONE); + } else { + this.content.setVisibility(View.VISIBLE); + } + } + + private void hidePoll() { + pollButton.setVisibility(View.GONE); + pollDescription.setVisibility(View.GONE); + pollOptions.setVisibility(View.GONE); + } + + private void setAvatar(String url, + @Nullable String rebloggedUrl, + boolean isBot, + StatusDisplayOptions statusDisplayOptions) { + + int avatarRadius; + if (TextUtils.isEmpty(rebloggedUrl)) { + avatar.setPaddingRelative(0, 0, 0, 0); + + if (statusDisplayOptions.showBotOverlay() && isBot) { + avatarInset.setVisibility(View.VISIBLE); + Glide.with(avatarInset) + .load(R.drawable.bot_badge) + .into(avatarInset); + } else { + avatarInset.setVisibility(View.GONE); + } + + avatarRadius = avatarRadius48dp; + + } else { + int padding = Utils.dpToPx(avatar.getContext(), 12); + avatar.setPaddingRelative(0, 0, padding, padding); + + avatarInset.setVisibility(View.VISIBLE); + avatarInset.setBackground(null); + ImageLoadingHelper.loadAvatar(rebloggedUrl, avatarInset, avatarRadius24dp, + statusDisplayOptions.animateAvatars(), null); + + avatarRadius = avatarRadius36dp; + } + + ImageLoadingHelper.loadAvatar( + url, + avatar, + avatarRadius, + statusDisplayOptions.animateAvatars(), + Collections.singletonList(new CompositeWithOpaqueBackground(MaterialColors.getColor(avatar, android.R.attr.colorBackground))) + ); + } + + protected void setMetaData(@NonNull StatusViewData.Concrete statusViewData, @NonNull StatusDisplayOptions statusDisplayOptions, @NonNull StatusActionListener listener) { + + Status status = statusViewData.getActionable(); + Date createdAt = status.getCreatedAt(); + Date editedAt = status.getEditedAt(); + + String timestampText; + if (statusDisplayOptions.useAbsoluteTime()) { + timestampText = absoluteTimeFormatter.format(createdAt, true); + } else { + if (createdAt == null) { + timestampText = "?m"; + } else { + long then = createdAt.getTime(); + long now = System.currentTimeMillis(); + timestampText = TimestampUtils.getRelativeTimeSpanString(metaInfo.getContext(), then, now); + } + } + + if (editedAt != null) { + timestampText = metaInfo.getContext().getString(R.string.post_timestamp_with_edited_indicator, timestampText); + } + metaInfo.setText(timestampText); + } + + private CharSequence getCreatedAtDescription(Date createdAt, + StatusDisplayOptions statusDisplayOptions) { + if (statusDisplayOptions.useAbsoluteTime()) { + return absoluteTimeFormatter.format(createdAt, true); + } else { + /* This one is for screen-readers. Frequently, they would mispronounce timestamps like "17m" + * as 17 meters instead of minutes. */ + + if (createdAt == null) { + return "? minutes"; + } else { + long then = createdAt.getTime(); + long now = System.currentTimeMillis(); + return DateUtils.getRelativeTimeSpanString(then, now, + DateUtils.SECOND_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE); + } + } + } + + protected void setIsReply(boolean isReply) { + if (isReply) { + replyButton.setImageResource(R.drawable.ic_reply_all_24dp); + } else { + replyButton.setImageResource(R.drawable.ic_reply_24dp); + } + + } + + protected void setReplyCount(int repliesCount, boolean fullStats) { + // This label only exists in the non-detailed view (to match the web ui) + if (replyCountLabel == null) return; + + if (fullStats) { + replyCountLabel.setText(NumberUtils.formatNumber(repliesCount, 1000)); + return; + } + + // Show "0", "1", or "1+" for replies otherwise, so the user knows if there is a thread + // that they can click through to read. + replyCountLabel.setText((repliesCount > 1 ? replyCountLabel.getContext().getString(R.string.status_count_one_plus) : Integer.toString(repliesCount))); + } + + private void setReblogged(boolean reblogged) { + reblogButton.setChecked(reblogged); + } + + // This should only be called after setReblogged, in order to override the tint correctly. + private void setRebloggingEnabled(boolean enabled, Status.Visibility visibility) { + reblogButton.setEnabled(enabled && visibility != Status.Visibility.PRIVATE); + + if (enabled) { + int inactiveId; + int activeId; + if (visibility == Status.Visibility.PRIVATE) { + inactiveId = R.drawable.ic_reblog_private_24dp; + activeId = R.drawable.ic_reblog_private_active_24dp; + } else { + inactiveId = R.drawable.ic_reblog_24dp; + activeId = R.drawable.ic_reblog_active_24dp; + } + reblogButton.setInactiveImage(inactiveId); + reblogButton.setActiveImage(activeId); + } else { + int disabledId; + if (visibility == Status.Visibility.DIRECT) { + disabledId = R.drawable.ic_reblog_direct_24dp; + } else { + disabledId = R.drawable.ic_reblog_private_24dp; + } + reblogButton.setInactiveImage(disabledId); + reblogButton.setActiveImage(disabledId); + } + } + + protected void setFavourited(boolean favourited) { + favouriteButton.setChecked(favourited); + } + + protected void setBookmarked(boolean bookmarked) { + bookmarkButton.setChecked(bookmarked); + } + + private BitmapDrawable decodeBlurHash(String blurhash) { + return ImageLoadingHelper.decodeBlurHash(this.avatar.getContext(), blurhash); + } + + private void loadImage(MediaPreviewImageView imageView, + @Nullable String previewUrl, + @Nullable MetaData meta, + @Nullable String blurhash) { + + Drawable placeholder = blurhash != null ? decodeBlurHash(blurhash) : mediaPreviewUnloaded; + + if (TextUtils.isEmpty(previewUrl)) { + imageView.removeFocalPoint(); + + Glide.with(imageView) + .load(placeholder) + .centerInside() + .into(imageView); + } else { + Focus focus = meta != null ? meta.getFocus() : null; + + if (focus != null) { // If there is a focal point for this attachment: + imageView.setFocalPoint(focus); + + Glide.with(imageView.getContext()) + .load(previewUrl) + .placeholder(placeholder) + .centerInside() + .addListener(imageView) + .into(imageView); + } else { + imageView.removeFocalPoint(); + + Glide.with(imageView) + .load(previewUrl) + .placeholder(placeholder) + .centerInside() + .into(imageView); + } + } + } + + protected void setMediaPreviews( + final @NonNull List<Attachment> attachments, + boolean sensitive, + final @NonNull StatusActionListener listener, + boolean showingContent, + boolean useBlurhash + ) { + + mediaPreview.setVisibility(View.VISIBLE); + mediaPreview.setAspectRatios(AttachmentHelper.aspectRatios(attachments)); + + mediaPreview.forEachIndexed((i, imageView, descriptionIndicator) -> { + Attachment attachment = attachments.get(i); + String previewUrl = attachment.getPreviewUrl(); + String description = attachment.getDescription(); + boolean hasDescription = !TextUtils.isEmpty(description); + + if (hasDescription) { + imageView.setContentDescription(description); + } else { + imageView.setContentDescription(imageView.getContext().getString(R.string.action_view_media)); + } + + loadImage( + imageView, + showingContent ? previewUrl : null, + attachment.getMeta(), + useBlurhash ? attachment.getBlurhash() : null + ); + + final Attachment.Type type = attachment.getType(); + if (showingContent && (type == Attachment.Type.VIDEO || type == Attachment.Type.GIFV)) { + imageView.setForegroundGravity(Gravity.CENTER); + imageView.setForeground(AppCompatResources.getDrawable(itemView.getContext(), R.drawable.ic_play_indicator)); + } else { + imageView.setForeground(null); + } + + final CharSequence formattedDescription = AttachmentHelper.getFormattedDescription(attachment, imageView.getContext()); + setAttachmentClickListener(imageView, listener, i, formattedDescription, true); + + if (sensitive) { + sensitiveMediaWarning.setText(R.string.post_sensitive_media_title); + } else { + sensitiveMediaWarning.setText(R.string.post_media_hidden_title); + } + + sensitiveMediaWarning.setVisibility(showingContent ? View.GONE : View.VISIBLE); + sensitiveMediaShow.setVisibility(showingContent ? View.VISIBLE : View.GONE); + + descriptionIndicator.setVisibility(hasDescription && showingContent ? View.VISIBLE : View.GONE); + + sensitiveMediaShow.setOnClickListener(v -> { + if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onContentHiddenChange(false, getBindingAdapterPosition()); + } + v.setVisibility(View.GONE); + sensitiveMediaWarning.setVisibility(View.VISIBLE); + descriptionIndicator.setVisibility(View.GONE); + }); + sensitiveMediaWarning.setOnClickListener(v -> { + if (getBindingAdapterPosition() != RecyclerView.NO_POSITION) { + listener.onContentHiddenChange(true, getBindingAdapterPosition()); + } + v.setVisibility(View.GONE); + sensitiveMediaShow.setVisibility(View.VISIBLE); + descriptionIndicator.setVisibility(hasDescription ? View.VISIBLE : View.GONE); + }); + + return null; + }); + } + + @DrawableRes + private static int getLabelIcon(Attachment.Type type) { + switch (type) { + case IMAGE: + return R.drawable.ic_photo_24dp; + case GIFV: + case VIDEO: + return R.drawable.ic_videocam_24dp; + case AUDIO: + return R.drawable.ic_music_box_24dp; + default: + return R.drawable.ic_attach_file_24dp; + } + } + + private void updateMediaLabel(int index, boolean sensitive, boolean showingContent) { + Context context = itemView.getContext(); + CharSequence label = (sensitive && !showingContent) ? + context.getString(R.string.post_sensitive_media_title) : + mediaDescriptions[index]; + mediaLabels[index].setText(label); + } + + protected void setMediaLabel(@NonNull List<Attachment> attachments, boolean sensitive, + final @NonNull StatusActionListener listener, boolean showingContent) { + Context context = itemView.getContext(); + for (int i = 0; i < mediaLabels.length; i++) { + TextView mediaLabel = mediaLabels[i]; + if (i < attachments.size()) { + Attachment attachment = attachments.get(i); + mediaLabel.setVisibility(View.VISIBLE); + mediaDescriptions[i] = AttachmentHelper.getFormattedDescription(attachment, context); + updateMediaLabel(i, sensitive, showingContent); + + // Set the icon next to the label. + int drawableId = getLabelIcon(attachments.get(0).getType()); + mediaLabel.setCompoundDrawablesRelativeWithIntrinsicBounds(drawableId, 0, 0, 0); + + setAttachmentClickListener(mediaLabel, listener, i, mediaDescriptions[i], false); + } else { + mediaLabel.setVisibility(View.GONE); + } + } + } + + private void setAttachmentClickListener(@NonNull View view, @NonNull StatusActionListener listener, + int index, CharSequence description, boolean animateTransition) { + view.setOnClickListener(v -> { + int position = getBindingAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + if (sensitiveMediaWarning.getVisibility() == View.VISIBLE) { + listener.onContentHiddenChange(true, getBindingAdapterPosition()); + } else { + listener.onViewMedia(position, index, animateTransition ? v : null); + } + } + }); + TooltipCompat.setTooltipText(view, description); + } + + protected void hideSensitiveMediaWarning() { + sensitiveMediaWarning.setVisibility(View.GONE); + sensitiveMediaShow.setVisibility(View.GONE); + } + + protected void setupButtons(final @NonNull StatusActionListener listener, + final @NonNull String accountId, + final @Nullable String statusContent, + @NonNull StatusDisplayOptions statusDisplayOptions) { + View.OnClickListener profileButtonClickListener = button -> listener.onViewAccount(accountId); + + avatar.setOnClickListener(profileButtonClickListener); + displayName.setOnClickListener(profileButtonClickListener); + + replyButton.setOnClickListener(v -> { + int position = getBindingAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onReply(position); + } + }); + + + if (reblogButton != null) { + reblogButton.setEventListener((button, buttonState) -> { + // return true to play animation + int position = getBindingAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + if (statusDisplayOptions.confirmReblogs()) { + showConfirmReblog(listener, buttonState, position); + return false; + } else { + listener.onReblog(!buttonState, position); + return true; + } + } else { + return false; + } + }); + } + + + favouriteButton.setEventListener((button, buttonState) -> { + // return true to play animation + int position = getBindingAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + if (statusDisplayOptions.confirmFavourites()) { + showConfirmFavourite(listener, buttonState, position); + return false; + } else { + listener.onFavourite(!buttonState, position); + return true; + } + } else { + return true; + } + }); + + bookmarkButton.setEventListener((button, buttonState) -> { + int position = getBindingAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onBookmark(!buttonState, position); + } + return true; + }); + + moreButton.setOnClickListener(v -> { + int position = getBindingAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onMore(v, position); + } + }); + /* Even though the content TextView is a child of the container, it won't respond to clicks + * if it contains URLSpans without also setting its listener. The surrounding spans will + * just eat the clicks instead of deferring to the parent listener, but WILL respond to a + * listener directly on the TextView, for whatever reason. */ + View.OnClickListener viewThreadListener = v -> { + int position = getBindingAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onViewThread(position); + } + }; + content.setOnClickListener(viewThreadListener); + itemView.setOnClickListener(viewThreadListener); + } + + private void showConfirmReblog(StatusActionListener listener, + boolean buttonState, + int position) { + PopupMenu popup = new PopupMenu(itemView.getContext(), reblogButton); + popup.inflate(R.menu.status_reblog); + Menu menu = popup.getMenu(); + if (buttonState) { + menu.findItem(R.id.menu_action_reblog).setVisible(false); + } else { + menu.findItem(R.id.menu_action_unreblog).setVisible(false); + } + popup.setOnMenuItemClickListener(item -> { + listener.onReblog(!buttonState, position); + if (!buttonState) { + reblogButton.playAnimation(); + reblogButton.setChecked(true); + } + return true; + }); + popup.show(); + } + + private void showConfirmFavourite(StatusActionListener listener, + boolean buttonState, + int position) { + PopupMenu popup = new PopupMenu(itemView.getContext(), favouriteButton); + popup.inflate(R.menu.status_favourite); + Menu menu = popup.getMenu(); + if (buttonState) { + menu.findItem(R.id.menu_action_favourite).setVisible(false); + } else { + menu.findItem(R.id.menu_action_unfavourite).setVisible(false); + } + popup.setOnMenuItemClickListener(item -> { + listener.onFavourite(!buttonState, position); + if (!buttonState) { + favouriteButton.playAnimation(); + favouriteButton.setChecked(true); + } + return true; + }); + popup.show(); + } + + public void setupWithStatus(@NonNull StatusViewData.Concrete status, final @NonNull StatusActionListener listener, + @NonNull StatusDisplayOptions statusDisplayOptions) { + this.setupWithStatus(status, listener, statusDisplayOptions, null); + } + + public void setupWithStatus(@NonNull StatusViewData.Concrete status, + @NonNull final StatusActionListener listener, + @NonNull StatusDisplayOptions statusDisplayOptions, + @Nullable Object payloads) { + if (payloads == null) { + Status actionable = status.getActionable(); + setDisplayName(actionable.getAccount().getName(), actionable.getAccount().getEmojis(), statusDisplayOptions); + setUsername(actionable.getAccount().getUsername()); + setMetaData(status, statusDisplayOptions, listener); + setIsReply(actionable.getInReplyToId() != null); + setReplyCount(actionable.getRepliesCount(), statusDisplayOptions.showStatsInline()); + setAvatar(actionable.getAccount().getAvatar(), status.getRebloggedAvatar(), + actionable.getAccount().getBot(), statusDisplayOptions); + setReblogged(actionable.getReblogged()); + setFavourited(actionable.getFavourited()); + setBookmarked(actionable.getBookmarked()); + List<Attachment> attachments = status.getAttachments(); + boolean sensitive = actionable.getSensitive(); + if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { + setMediaPreviews(attachments, sensitive, listener, status.isShowingContent(), statusDisplayOptions.useBlurhash()); + + if (attachments.isEmpty()) { + hideSensitiveMediaWarning(); + } + // Hide the unused label. + for (TextView mediaLabel : mediaLabels) { + mediaLabel.setVisibility(View.GONE); + } + } else { + setMediaLabel(attachments, sensitive, listener, status.isShowingContent()); + // Hide all unused views. + mediaPreview.setVisibility(View.GONE); + hideSensitiveMediaWarning(); + } + + setupCard(status, status.isExpanded(), statusDisplayOptions.cardViewMode(), statusDisplayOptions, listener); + + setupButtons(listener, actionable.getAccount().getId(), status.getContent().toString(), + statusDisplayOptions); + + setTranslationStatus(status, listener); + + setRebloggingEnabled(actionable.isRebloggingAllowed(), actionable.getVisibility()); + + setSpoilerAndContent(status, statusDisplayOptions, listener); + + setupFilterPlaceholder(status, listener, statusDisplayOptions); + + setDescriptionForStatus(status, statusDisplayOptions); + + // Workaround for RecyclerView 1.0.0 / androidx.core 1.0.0 + // RecyclerView tries to set AccessibilityDelegateCompat to null + // but ViewCompat code replaces is with the default one. RecyclerView never + // fetches another one from its delegate because it checks that it's set so we remove it + // and let RecyclerView ask for a new delegate. + itemView.setAccessibilityDelegate(null); + } else { + if (payloads instanceof List) + for (Object item : (List<?>) payloads) { + if (Key.KEY_CREATED.equals(item)) { + setMetaData(status, statusDisplayOptions, listener); + } + } + + } + } + + private void setTranslationStatus(StatusViewData.Concrete status, StatusActionListener listener) { + var translationViewData = status.getTranslation(); + if (translationViewData != null) { + if (translationViewData instanceof TranslationViewData.Loaded) { + Translation translation = ((TranslationViewData.Loaded) translationViewData).getData(); + translationStatusView.setVisibility(View.VISIBLE); + var langName = LocaleUtilsKt.localeNameForUntrustedISO639LangCode(translation.getDetectedSourceLanguage()); + translationStatusView.setText(translationStatusView.getContext().getString(R.string.label_translated, langName, translation.getProvider())); + untranslateButton.setVisibility(View.VISIBLE); + untranslateButton.setOnClickListener((v) -> listener.onUntranslate(getBindingAdapterPosition())); + } else { + translationStatusView.setVisibility(View.VISIBLE); + translationStatusView.setText(R.string.label_translating); + untranslateButton.setVisibility(View.GONE); + untranslateButton.setOnClickListener(null); + } + } else { + translationStatusView.setVisibility(View.GONE); + untranslateButton.setVisibility(View.GONE); + untranslateButton.setOnClickListener(null); + } + } + + private void setupFilterPlaceholder(StatusViewData.Concrete status, StatusActionListener listener, StatusDisplayOptions displayOptions) { + if (status.getFilterAction() != Filter.Action.WARN) { + showFilteredPlaceholder(false); + return; + } + + showFilteredPlaceholder(true); + + Filter matchedFilter = null; + + for (FilterResult result : status.getActionable().getFiltered()) { + Filter filter = result.getFilter(); + if (filter.getAction() == Filter.Action.WARN) { + matchedFilter = filter; + break; + } + } + + final String matchedFilterTitle; + if (matchedFilter == null) { + matchedFilterTitle = ""; + } else { + matchedFilterTitle = matchedFilter.getTitle(); + } + + filteredPlaceholderLabel.setText(itemView.getContext().getString(R.string.status_filter_placeholder_label_format, matchedFilterTitle)); + filteredPlaceholderShowButton.setOnClickListener(view -> listener.clearWarningAction(getBindingAdapterPosition())); + } + + protected static boolean hasPreviewableAttachment(@NonNull List<Attachment> attachments) { + for (Attachment attachment : attachments) { + if (attachment.getType() == Attachment.Type.AUDIO || attachment.getType() == Attachment.Type.UNKNOWN) { + return false; + } + } + return true; + } + + private void setDescriptionForStatus(@NonNull StatusViewData.Concrete status, + StatusDisplayOptions statusDisplayOptions) { + Context context = itemView.getContext(); + Status actionable = status.getActionable(); + + String description = context.getString(R.string.description_status, + // 1 display_name + actionable.getAccount().getDisplayName(), + // 2 CW? + getContentWarningDescription(context, status), + // 3 content? + (TextUtils.isEmpty(status.getSpoilerText()) || !actionable.getSensitive() || status.isExpanded() ? status.getContent() : ""), + // 4 date + getCreatedAtDescription(actionable.getCreatedAt(), statusDisplayOptions), + // 5 edited? + actionable.getEditedAt() != null ? context.getString(R.string.description_post_edited) : "", + // 6 reposted_by? + getReblogDescription(context, status), + // 7 username + actionable.getAccount().getUsername(), + // 8 reposted + actionable.getReblogged() ? context.getString(R.string.description_post_reblogged) : "", + // 9 favorited + actionable.getFavourited() ? context.getString(R.string.description_post_favourited) : "", + // 10 bookmarked + actionable.getBookmarked() ? context.getString(R.string.description_post_bookmarked) : "", + // 11 media + getMediaDescription(context, status), + // 12 visibility + getVisibilityDescription(context, actionable.getVisibility()), + // 13 fav_number + getFavsText(context, actionable.getFavouritesCount()), + // 14 reblog_number + getReblogsText(context, actionable.getReblogsCount()), + // 15 poll? + getPollDescription(status, context, statusDisplayOptions), + // 16 translated? + getTranslatedDescription(context, status.getTranslation()) + ); + itemView.setContentDescription(description); + } + + private String getTranslatedDescription(Context context, TranslationViewData translationViewData) { + if (translationViewData == null) { + return ""; + } else if (translationViewData instanceof TranslationViewData.Loading) { + return context.getString(R.string.label_translating); + } else { + Translation translation = ((TranslationViewData.Loaded) translationViewData).getData(); + var langName = LocaleUtilsKt.localeNameForUntrustedISO639LangCode(translation.getDetectedSourceLanguage()); + return context.getString(R.string.label_translated, langName, translation.getProvider()); + } + } + + private static CharSequence getReblogDescription(Context context, + @NonNull StatusViewData.Concrete status) { + @Nullable + Status reblog = status.getRebloggingStatus(); + if (reblog != null) { + return context + .getString(R.string.post_boosted_format, reblog.getAccount().getUsername()); + } else { + return ""; + } + } + + private static CharSequence getMediaDescription(Context context, + @NonNull StatusViewData.Concrete viewData) { + if (viewData.getAttachments().isEmpty()) { + return ""; + } + StringBuilder mediaDescriptions = CollectionsKt.fold( + viewData.getAttachments(), + new StringBuilder(), + (builder, a) -> { + if (a.getDescription() == null) { + String placeholder = + context.getString(R.string.description_post_media_no_description_placeholder); + return builder.append(placeholder); + } else { + builder.append("; "); + return builder.append(a.getDescription()); + } + }); + return context.getString(R.string.description_post_media, mediaDescriptions); + } + + private static CharSequence getContentWarningDescription(Context context, + @NonNull StatusViewData.Concrete status) { + if (!TextUtils.isEmpty(status.getSpoilerText())) { + return context.getString(R.string.description_post_cw, status.getSpoilerText()); + } else { + return ""; + } + } + + @NonNull + protected static CharSequence getVisibilityDescription(@NonNull Context context, @Nullable Status.Visibility visibility) { + + if (visibility == null) { + return ""; + } + + int resource; + switch (visibility) { + case PUBLIC: + resource = R.string.description_visibility_public; + break; + case UNLISTED: + resource = R.string.description_visibility_unlisted; + break; + case PRIVATE: + resource = R.string.description_visibility_private; + break; + case DIRECT: + resource = R.string.description_visibility_direct; + break; + default: + return ""; + } + return context.getString(resource); + } + + private CharSequence getPollDescription(@NonNull StatusViewData.Concrete status, + Context context, + StatusDisplayOptions statusDisplayOptions) { + PollViewData poll = PollViewDataKt.toViewData(status.getPoll()); + if (poll == null) { + return ""; + } else { + Object[] args = new CharSequence[5]; + List<PollOptionViewData> options = poll.getOptions(); + for (int i = 0; i < args.length; i++) { + if (i < options.size()) { + int percent = PollViewDataKt.calculatePercent(options.get(i).getVotesCount(), poll.getVotersCount(), poll.getVotesCount()); + args[i] = buildDescription(options.get(i).getTitle(), percent, options.get(i).getVoted(), context); + } else { + args[i] = ""; + } + } + args[4] = getPollInfoText(System.currentTimeMillis(), poll, statusDisplayOptions, + context); + return context.getString(R.string.description_poll, args); + } + } + + @NonNull + protected CharSequence getFavsText(@NonNull Context context, int count) { + String countString = numberFormat.format(count); + return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.favs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY); + } + + @NonNull + protected CharSequence getReblogsText(@NonNull Context context, int count) { + String countString = numberFormat.format(count); + return HtmlCompat.fromHtml(context.getResources().getQuantityString(R.plurals.reblogs, count, countString), HtmlCompat.FROM_HTML_MODE_LEGACY); + } + + private void setupPoll(PollViewData poll, List<Emoji> emojis, + StatusDisplayOptions statusDisplayOptions, + StatusActionListener listener) { + long timestamp = System.currentTimeMillis(); + + boolean expired = poll.getExpired() || (poll.getExpiresAt() != null && timestamp > poll.getExpiresAt().getTime()); + + Context context = pollDescription.getContext(); + + pollOptions.setVisibility(View.VISIBLE); + + if (expired || poll.getVoted()) { + // no voting possible + View.OnClickListener viewThreadListener = v -> { + int position = getBindingAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onViewThread(position); + } + }; + pollAdapter.setup( + poll.getOptions(), + poll.getVotesCount(), + poll.getVotersCount(), + emojis, + PollAdapter.RESULT, + viewThreadListener, + statusDisplayOptions.animateEmojis() + ); + + pollButton.setVisibility(View.GONE); + } else { + // voting possible + pollAdapter.setup( + poll.getOptions(), + poll.getVotesCount(), + poll.getVotersCount(), + emojis, + poll.getMultiple() ? PollAdapter.MULTIPLE : PollAdapter.SINGLE, + null, + statusDisplayOptions.animateEmojis() + ); + + pollButton.setVisibility(View.VISIBLE); + + pollButton.setOnClickListener(v -> { + + int position = getBindingAdapterPosition(); + + if (position != RecyclerView.NO_POSITION) { + + List<Integer> pollResult = pollAdapter.getSelected(); + + if (!pollResult.isEmpty()) { + listener.onVoteInPoll(position, pollResult); + } + } + + }); + } + + pollDescription.setVisibility(View.VISIBLE); + pollDescription.setText(getPollInfoText(timestamp, poll, statusDisplayOptions, context)); + } + + private CharSequence getPollInfoText(long timestamp, PollViewData poll, + StatusDisplayOptions statusDisplayOptions, + Context context) { + String votesText; + if (poll.getVotersCount() == null) { + String voters = numberFormat.format(poll.getVotesCount()); + votesText = context.getResources().getQuantityString(R.plurals.poll_info_votes, poll.getVotesCount(), voters); + } else { + String voters = numberFormat.format(poll.getVotersCount()); + votesText = context.getResources().getQuantityString(R.plurals.poll_info_people, poll.getVotersCount(), voters); + } + CharSequence pollDurationInfo; + if (poll.getExpired()) { + pollDurationInfo = context.getString(R.string.poll_info_closed); + } else if (poll.getExpiresAt() == null) { + return votesText; + } else { + if (statusDisplayOptions.useAbsoluteTime()) { + pollDurationInfo = context.getString(R.string.poll_info_time_absolute, absoluteTimeFormatter.format(poll.getExpiresAt(), false)); + } else { + pollDurationInfo = TimestampUtils.formatPollDuration(pollDescription.getContext(), poll.getExpiresAt().getTime(), timestamp); + } + } + + return pollDescription.getContext().getString(R.string.poll_info_format, votesText, pollDurationInfo); + } + + protected void setupCard( + final @NonNull StatusViewData.Concrete status, + boolean expanded, + final @NonNull CardViewMode cardViewMode, + final @NonNull StatusDisplayOptions statusDisplayOptions, + final @NonNull StatusActionListener listener + ) { + if (cardView == null) { + return; + } + + final Status actionable = status.getActionable(); + final Card card = actionable.getCard(); + + if (cardViewMode != CardViewMode.NONE && + actionable.getAttachments().isEmpty() && + actionable.getPoll() == null && + card != null && + !TextUtils.isEmpty(card.getUrl()) && + (TextUtils.isEmpty(actionable.getSpoilerText()) || expanded) && + (!status.isCollapsible() || !status.isCollapsed())) { + + cardView.setVisibility(View.VISIBLE); + cardTitle.setText(card.getTitle()); + if (TextUtils.isEmpty(card.getDescription()) && TextUtils.isEmpty(card.getAuthorName())) { + cardDescription.setVisibility(View.GONE); + } else { + cardDescription.setVisibility(View.VISIBLE); + if (TextUtils.isEmpty(card.getDescription())) { + cardDescription.setText(card.getAuthorName()); + } else { + cardDescription.setText(card.getDescription()); + } + } + + cardUrl.setText(card.getUrl()); + + // Statuses from other activitypub sources can be marked sensitive even if there's no media, + // so let's blur the preview in that case + // If media previews are disabled, show placeholder for cards as well + if (statusDisplayOptions.mediaPreviewEnabled() && !actionable.getSensitive() && !TextUtils.isEmpty(card.getImage())) { + + int radius = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_radius); + ShapeAppearanceModel.Builder cardImageShape = ShapeAppearanceModel.builder(); + + if (card.getWidth() > card.getHeight()) { + cardView.setOrientation(LinearLayout.VERTICAL); + + cardImage.getLayoutParams().height = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_image_vertical_height); + cardImage.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT; + cardImageShape.setTopLeftCorner(CornerFamily.ROUNDED, radius); + cardImageShape.setTopRightCorner(CornerFamily.ROUNDED, radius); + } else { + cardView.setOrientation(LinearLayout.HORIZONTAL); + cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + cardImage.getLayoutParams().width = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_image_horizontal_width); + cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; + cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + cardImageShape.setTopLeftCorner(CornerFamily.ROUNDED, radius); + cardImageShape.setBottomLeftCorner(CornerFamily.ROUNDED, radius); + } + + cardImage.setShapeAppearanceModel(cardImageShape.build()); + + cardImage.setScaleType(ImageView.ScaleType.CENTER_CROP); + + RequestBuilder<Drawable> builder = Glide.with(cardImage.getContext()) + .load(card.getImage()) + .dontTransform(); + if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) { + builder = builder.placeholder(decodeBlurHash(card.getBlurhash())); + } + builder.into(cardImage); + } else if (statusDisplayOptions.useBlurhash() && !TextUtils.isEmpty(card.getBlurhash())) { + int radius = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_radius); + + cardView.setOrientation(LinearLayout.HORIZONTAL); + cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + cardImage.getLayoutParams().width = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_image_horizontal_width); + cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; + cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + + ShapeAppearanceModel cardImageShape = ShapeAppearanceModel.builder() + .setTopLeftCorner(CornerFamily.ROUNDED, radius) + .setBottomLeftCorner(CornerFamily.ROUNDED, radius) + .build(); + cardImage.setShapeAppearanceModel(cardImageShape); + + cardImage.setScaleType(ImageView.ScaleType.CENTER_CROP); + + Glide.with(cardImage.getContext()) + .load(decodeBlurHash(card.getBlurhash())) + .dontTransform() + .into(cardImage); + } else { + cardView.setOrientation(LinearLayout.HORIZONTAL); + cardImage.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT; + cardImage.getLayoutParams().width = cardImage.getContext().getResources() + .getDimensionPixelSize(R.dimen.card_image_horizontal_width); + cardInfo.getLayoutParams().height = ViewGroup.LayoutParams.WRAP_CONTENT; + cardInfo.getLayoutParams().width = ViewGroup.LayoutParams.MATCH_PARENT; + + cardImage.setShapeAppearanceModel(new ShapeAppearanceModel()); + + cardImage.setScaleType(ImageView.ScaleType.CENTER); + + Glide.with(cardImage.getContext()) + .load(R.drawable.card_image_placeholder) + .into(cardImage); + } + + View.OnClickListener visitLink = v -> listener.onViewUrl(card.getUrl()); + + cardView.setOnClickListener(visitLink); + // View embedded photos in our image viewer instead of opening the browser + cardImage.setOnClickListener(card.getType().equals(Card.TYPE_PHOTO) && !TextUtils.isEmpty(card.getEmbedUrl()) ? + v -> cardView.getContext().startActivity(ViewMediaActivity.newSingleImageIntent(cardView.getContext(), card.getEmbedUrl())) : + visitLink); + + cardView.setClipToOutline(true); + } else { + cardView.setVisibility(View.GONE); + } + } + + public void showStatusContent(boolean show) { + int visibility = show ? View.VISIBLE : View.GONE; + avatar.setVisibility(visibility); + avatarInset.setVisibility(visibility); + displayName.setVisibility(visibility); + username.setVisibility(visibility); + metaInfo.setVisibility(visibility); + contentWarningDescription.setVisibility(visibility); + contentWarningButton.setVisibility(visibility); + content.setVisibility(visibility); + cardView.setVisibility(visibility); + mediaContainer.setVisibility(visibility); + pollOptions.setVisibility(visibility); + pollButton.setVisibility(visibility); + pollDescription.setVisibility(visibility); + replyButton.setVisibility(visibility); + reblogButton.setVisibility(visibility); + favouriteButton.setVisibility(visibility); + bookmarkButton.setVisibility(visibility); + moreButton.setVisibility(visibility); + } + + public void showFilteredPlaceholder(boolean show) { + if (statusContainer != null) { + statusContainer.setVisibility(show ? View.GONE : View.VISIBLE); + } + if (filteredPlaceholder != null) { + filteredPlaceholder.setVisibility(show ? View.VISIBLE : View.GONE); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java new file mode 100644 index 0000000..d8921c2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusDetailedViewHolder.java @@ -0,0 +1,211 @@ +package com.keylesspalace.tusky.adapter; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.method.LinkMovementMethod; +import android.text.style.DynamicDrawableSpan; +import android.text.style.ImageSpan; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.content.res.AppCompatResources; +import androidx.recyclerview.widget.RecyclerView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CardViewMode; +import com.keylesspalace.tusky.util.LinkHelper; +import com.keylesspalace.tusky.util.NoUnderlineURLSpan; +import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.viewdata.StatusViewData; + +import java.text.DateFormat; +import java.util.Date; + +public class StatusDetailedViewHolder extends StatusBaseViewHolder { + private final TextView reblogs; + private final TextView favourites; + private final View infoDivider; + + private static final DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.SHORT); + + public StatusDetailedViewHolder(@NonNull View view) { + super(view); + reblogs = view.findViewById(R.id.status_reblogs); + favourites = view.findViewById(R.id.status_favourites); + infoDivider = view.findViewById(R.id.status_info_divider); + } + + @Override + protected void setMetaData(@NonNull StatusViewData.Concrete statusViewData, @NonNull StatusDisplayOptions statusDisplayOptions, @NonNull StatusActionListener listener) { + + Status status = statusViewData.getActionable(); + + Status.Visibility visibility = status.getVisibility(); + Context context = metaInfo.getContext(); + + Drawable visibilityIcon = getVisibilityIcon(visibility); + CharSequence visibilityString = getVisibilityDescription(context, visibility); + + SpannableStringBuilder sb = new SpannableStringBuilder(visibilityString); + + if (visibilityIcon != null) { + ImageSpan visibilityIconSpan = new ImageSpan( + visibilityIcon, + Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q ? DynamicDrawableSpan.ALIGN_CENTER : DynamicDrawableSpan.ALIGN_BASELINE + ); + sb.setSpan(visibilityIconSpan, 0, visibilityString.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + String metadataJoiner = context.getString(R.string.metadata_joiner); + + Date createdAt = status.getCreatedAt(); + if (createdAt != null) { + sb.append(" "); + sb.append(dateFormat.format(createdAt)); + } + + Date editedAt = status.getEditedAt(); + + if (editedAt != null) { + String editedAtString = context.getString(R.string.post_edited, dateFormat.format(editedAt)); + + sb.append(metadataJoiner); + int spanStart = sb.length(); + int spanEnd = spanStart + editedAtString.length(); + + sb.append(editedAtString); + + if (statusViewData.getStatus().getEditedAt() != null) { + NoUnderlineURLSpan editedClickSpan = new NoUnderlineURLSpan("") { + @Override + public void onClick(@NonNull View view) { + listener.onShowEdits(getBindingAdapterPosition()); + } + }; + + sb.setSpan(editedClickSpan, spanStart, spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + String language = status.getLanguage(); + + if (language != null) { + sb.append(metadataJoiner); + sb.append(language.toUpperCase()); + } + + Status.Application app = status.getApplication(); + + if (app != null) { + sb.append(metadataJoiner); + + if (app.getWebsite() != null) { + CharSequence text = LinkHelper.createClickableText(app.getName(), app.getWebsite()); + sb.append(text); + } else { + sb.append(app.getName()); + } + } + + metaInfo.setMovementMethod(LinkMovementMethod.getInstance()); + metaInfo.setText(sb); + } + + private void setReblogAndFavCount(int reblogCount, int favCount, StatusActionListener listener) { + reblogs.setText(getReblogsText(reblogs.getContext(), reblogCount)); + favourites.setText(getFavsText(favourites.getContext(), favCount)); + + reblogs.setOnClickListener(v -> { + int position = getBindingAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onShowReblogs(position); + } + }); + favourites.setOnClickListener(v -> { + int position = getBindingAdapterPosition(); + if (position != RecyclerView.NO_POSITION) { + listener.onShowFavs(position); + } + }); + } + + @Override + public void setupWithStatus(@NonNull final StatusViewData.Concrete status, + @NonNull final StatusActionListener listener, + @NonNull StatusDisplayOptions statusDisplayOptions, + @Nullable Object payloads) { + // We never collapse statuses in the detail view + StatusViewData.Concrete uncollapsedStatus = (status.isCollapsible() && status.isCollapsed()) ? + status.copyWithCollapsed(false) : + status; + + super.setupWithStatus(uncollapsedStatus, listener, statusDisplayOptions, payloads); + setupCard(uncollapsedStatus, status.isExpanded(), CardViewMode.FULL_WIDTH, statusDisplayOptions, listener); // Always show card for detailed status + if (payloads == null) { + Status actionable = uncollapsedStatus.getActionable(); + + if (!statusDisplayOptions.hideStats()) { + setReblogAndFavCount(actionable.getReblogsCount(), + actionable.getFavouritesCount(), listener); + } else { + hideQuantitativeStats(); + } + } + } + + private @Nullable Drawable getVisibilityIcon(@Nullable Status.Visibility visibility) { + + if (visibility == null) { + return null; + } + + int visibilityIcon; + switch (visibility) { + case PUBLIC: + visibilityIcon = R.drawable.ic_public_24dp; + break; + case UNLISTED: + visibilityIcon = R.drawable.ic_lock_open_24dp; + break; + case PRIVATE: + visibilityIcon = R.drawable.ic_lock_outline_24dp; + break; + case DIRECT: + visibilityIcon = R.drawable.ic_email_24dp; + break; + default: + return null; + } + + final Drawable visibilityDrawable = AppCompatResources.getDrawable( + this.metaInfo.getContext(), visibilityIcon + ); + if (visibilityDrawable == null) { + return null; + } + + final int size = (int) this.metaInfo.getTextSize(); + visibilityDrawable.setBounds( + 0, + 0, + size, + size + ); + visibilityDrawable.setTint(this.metaInfo.getCurrentTextColor()); + + return visibilityDrawable; + } + + private void hideQuantitativeStats() { + reblogs.setVisibility(View.GONE); + favourites.setVisibility(View.GONE); + infoDivider.setVisibility(View.GONE); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java new file mode 100644 index 0000000..327f7cf --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/StatusViewHolder.java @@ -0,0 +1,170 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.adapter; + +import android.content.Context; +import android.text.InputFilter; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.entity.Emoji; +import com.keylesspalace.tusky.entity.Filter; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.CustomEmojiHelper; +import com.keylesspalace.tusky.util.NumberUtils; +import com.keylesspalace.tusky.util.SmartLengthInputFilter; +import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.util.StringUtils; +import com.keylesspalace.tusky.viewdata.StatusViewData; + +import java.util.List; + +import at.connyduck.sparkbutton.helpers.Utils; + +public class StatusViewHolder extends StatusBaseViewHolder { + private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE}; + private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0]; + + private final TextView statusInfo; + private final Button contentCollapseButton; + private final TextView favouritedCountLabel; + private final TextView reblogsCountLabel; + + public StatusViewHolder(@NonNull View itemView) { + super(itemView); + statusInfo = itemView.findViewById(R.id.status_info); + contentCollapseButton = itemView.findViewById(R.id.button_toggle_content); + favouritedCountLabel = itemView.findViewById(R.id.status_favourites_count); + reblogsCountLabel = itemView.findViewById(R.id.status_insets); + } + + @Override + public void setupWithStatus(@NonNull StatusViewData.Concrete status, + @NonNull final StatusActionListener listener, + @NonNull StatusDisplayOptions statusDisplayOptions, + @Nullable Object payloads) { + if (payloads == null) { + + boolean sensitive = !TextUtils.isEmpty(status.getActionable().getSpoilerText()); + boolean expanded = status.isExpanded(); + + setupCollapsedState(sensitive, expanded, status, listener); + + Status reblogging = status.getRebloggingStatus(); + if (reblogging == null || status.getFilterAction() == Filter.Action.WARN) { + hideStatusInfo(); + } else { + String rebloggedByDisplayName = reblogging.getAccount().getName(); + setRebloggedByDisplayName(rebloggedByDisplayName, + reblogging.getAccount().getEmojis(), statusDisplayOptions); + statusInfo.setOnClickListener(v -> listener.onOpenReblog(getBindingAdapterPosition())); + } + + } + + reblogsCountLabel.setVisibility(statusDisplayOptions.showStatsInline() ? View.VISIBLE : View.INVISIBLE); + favouritedCountLabel.setVisibility(statusDisplayOptions.showStatsInline() ? View.VISIBLE : View.INVISIBLE); + setFavouritedCount(status.getActionable().getFavouritesCount()); + setReblogsCount(status.getActionable().getReblogsCount()); + + super.setupWithStatus(status, listener, statusDisplayOptions, payloads); + } + + private void setRebloggedByDisplayName(final CharSequence name, + final List<Emoji> accountEmoji, + final StatusDisplayOptions statusDisplayOptions) { + Context context = statusInfo.getContext(); + CharSequence wrappedName = StringUtils.unicodeWrap(name); + CharSequence boostedText = context.getString(R.string.post_boosted_format, wrappedName); + CharSequence emojifiedText = CustomEmojiHelper.emojify( + boostedText, accountEmoji, statusInfo, statusDisplayOptions.animateEmojis() + ); + statusInfo.setText(emojifiedText); + statusInfo.setVisibility(View.VISIBLE); + } + + // don't use this on the same ViewHolder as setRebloggedByDisplayName, will cause recycling issues as paddings are changed + protected void setPollInfo(final boolean ownPoll) { + statusInfo.setText(ownPoll ? R.string.poll_ended_created : R.string.poll_ended_voted); + statusInfo.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_poll_24dp, 0, 0, 0); + statusInfo.setCompoundDrawablePadding(Utils.dpToPx(statusInfo.getContext(), 10)); + statusInfo.setPaddingRelative(Utils.dpToPx(statusInfo.getContext(), 28), 0, 0, 0); + statusInfo.setVisibility(View.VISIBLE); + } + + protected void setReblogsCount(int reblogsCount) { + reblogsCountLabel.setText(NumberUtils.formatNumber(reblogsCount, 1000)); + } + + protected void setFavouritedCount(int favouritedCount) { + favouritedCountLabel.setText(NumberUtils.formatNumber(favouritedCount, 1000)); + } + + protected void hideStatusInfo() { + statusInfo.setVisibility(View.GONE); + } + + private void setupCollapsedState(boolean sensitive, + boolean expanded, + final StatusViewData.Concrete status, + final StatusActionListener listener) { + /* input filter for TextViews have to be set before text */ + if (status.isCollapsible() && (!sensitive || expanded)) { + contentCollapseButton.setOnClickListener(view -> { + int position = getBindingAdapterPosition(); + if (position != RecyclerView.NO_POSITION) + listener.onContentCollapsedChange(!status.isCollapsed(), position); + }); + + contentCollapseButton.setVisibility(View.VISIBLE); + if (status.isCollapsed()) { + contentCollapseButton.setText(R.string.post_content_warning_show_more); + content.setFilters(COLLAPSE_INPUT_FILTER); + } else { + contentCollapseButton.setText(R.string.post_content_warning_show_less); + content.setFilters(NO_INPUT_FILTER); + } + } else { + contentCollapseButton.setVisibility(View.GONE); + content.setFilters(NO_INPUT_FILTER); + } + } + + public void showStatusContent(boolean show) { + super.showStatusContent(show); + contentCollapseButton.setVisibility(show ? View.VISIBLE : View.GONE); + } + + @Override + protected void toggleExpandedState(boolean sensitive, + boolean expanded, + @NonNull StatusViewData.Concrete status, + @NonNull StatusDisplayOptions statusDisplayOptions, + @NonNull final StatusActionListener listener) { + + setupCollapsedState(sensitive, expanded, status, listener); + + super.toggleExpandedState(sensitive, expanded, status, statusDisplayOptions, listener); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt new file mode 100644 index 0000000..7643aec --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/TabAdapter.kt @@ -0,0 +1,164 @@ +/* Copyright 2019 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.adapter + +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.ViewGroup +import androidx.core.view.size +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import com.google.android.material.chip.Chip +import com.keylesspalace.tusky.HASHTAG +import com.keylesspalace.tusky.LIST +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.TabData +import com.keylesspalace.tusky.databinding.ItemTabPreferenceBinding +import com.keylesspalace.tusky.databinding.ItemTabPreferenceSmallBinding +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.setDrawableTint +import com.keylesspalace.tusky.util.show + +interface ItemInteractionListener { + fun onTabAdded(tab: TabData) + fun onTabRemoved(position: Int) + fun onStartDelete(viewHolder: RecyclerView.ViewHolder) + fun onStartDrag(viewHolder: RecyclerView.ViewHolder) + fun onActionChipClicked(tab: TabData, tabPosition: Int) + fun onChipClicked(tab: TabData, tabPosition: Int, chipPosition: Int) +} + +class TabAdapter( + private var data: List<TabData>, + private val small: Boolean, + private val listener: ItemInteractionListener, + private var removeButtonEnabled: Boolean = false +) : RecyclerView.Adapter<BindingHolder<ViewBinding>>() { + + fun updateData(newData: List<TabData>) { + this.data = newData + notifyDataSetChanged() + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder<ViewBinding> { + val binding = if (small) { + ItemTabPreferenceSmallBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + } else { + ItemTabPreferenceBinding.inflate(LayoutInflater.from(parent.context), parent, false) + } + return BindingHolder(binding) + } + + override fun onBindViewHolder(holder: BindingHolder<ViewBinding>, position: Int) { + val context = holder.itemView.context + val tab = data[position] + + if (small) { + val binding = holder.binding as ItemTabPreferenceSmallBinding + + binding.textView.setText(tab.text) + + binding.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(tab.icon, 0, 0, 0) + + binding.textView.setOnClickListener { + listener.onTabAdded(tab) + } + } else { + val binding = holder.binding as ItemTabPreferenceBinding + + if (tab.id == LIST) { + binding.textView.text = tab.arguments.getOrNull(1).orEmpty() + } else { + binding.textView.setText(tab.text) + } + + binding.textView.setCompoundDrawablesRelativeWithIntrinsicBounds(tab.icon, 0, 0, 0) + + binding.imageView.setOnTouchListener { _, event -> + if (event.action == MotionEvent.ACTION_DOWN) { + listener.onStartDrag(holder) + true + } else { + false + } + } + binding.removeButton.setOnClickListener { + listener.onTabRemoved(holder.bindingAdapterPosition) + } + binding.removeButton.isEnabled = removeButtonEnabled + setDrawableTint( + holder.itemView.context, + binding.removeButton.drawable, + (if (removeButtonEnabled) android.R.attr.textColorTertiary else R.attr.textColorDisabled) + ) + + if (tab.id == HASHTAG) { + binding.chipGroup.show() + + /* + * The chip group will always contain the actionChip (it is defined in the xml layout). + * The other dynamic chips are inserted in front of the actionChip. + * This code tries to reuse already added chips to reduce the number of Views created. + */ + tab.arguments.forEachIndexed { i, arg -> + + val chip = binding.chipGroup.getChildAt(i).takeUnless { it.id == R.id.actionChip } as Chip? + ?: Chip(context).apply { + setCloseIconResource(R.drawable.ic_cancel_24dp) + isCheckable = false + binding.chipGroup.addView(this, binding.chipGroup.size - 1) + } + + chip.text = arg + + if (tab.arguments.size <= 1) { + chip.isCloseIconVisible = false + chip.setOnClickListener(null) + } else { + chip.isCloseIconVisible = true + chip.setOnClickListener { + listener.onChipClicked(tab, holder.bindingAdapterPosition, i) + } + } + } + + while (binding.chipGroup.size - 1 > tab.arguments.size) { + binding.chipGroup.removeViewAt(tab.arguments.size) + } + + binding.actionChip.setOnClickListener { + listener.onActionChipClicked(tab, holder.bindingAdapterPosition) + } + } else { + binding.chipGroup.hide() + } + } + } + + override fun getItemCount() = data.size + + fun setRemoveButtonVisible(enabled: Boolean) { + if (removeButtonEnabled != enabled) { + removeButtonEnabled = enabled + notifyDataSetChanged() + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt new file mode 100644 index 0000000..8a78cb2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/CacheUpdater.kt @@ -0,0 +1,76 @@ +package com.keylesspalace.tusky.appstore + +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.entity.Poll +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch + +/** + * Updates the database cache in response to events. + * This is important for the home timeline and notifications to be up to date. + */ +@OptIn(ExperimentalStdlibApi::class) +class CacheUpdater @Inject constructor( + eventHub: EventHub, + accountManager: AccountManager, + appDatabase: AppDatabase, + moshi: Moshi +) { + + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + + private val timelineDao = appDatabase.timelineDao() + private val statusDao = appDatabase.timelineStatusDao() + private val notificationsDao = appDatabase.notificationsDao() + + init { + scope.launch { + eventHub.events.collect { event -> + val tuskyAccountId = accountManager.activeAccount?.id ?: return@collect + when (event) { + is StatusChangedEvent -> statusDao.update( + tuskyAccountId = tuskyAccountId, + status = event.status, + moshi = moshi + ) + + is UnfollowEvent -> timelineDao.removeStatusesAndReblogsByUser(tuskyAccountId, event.accountId) + + is BlockEvent -> removeAllByUser(tuskyAccountId, event.accountId) + is MuteEvent -> removeAllByUser(tuskyAccountId, event.accountId) + + is DomainMuteEvent -> { + timelineDao.deleteAllFromInstance(tuskyAccountId, event.instance) + notificationsDao.deleteAllFromInstance(tuskyAccountId, event.instance) + } + + is StatusDeletedEvent -> { + timelineDao.deleteAllWithStatus(tuskyAccountId, event.statusId) + notificationsDao.deleteAllWithStatus(tuskyAccountId, event.statusId) + } + + is PollVoteEvent -> { + val pollString = moshi.adapter<Poll>().toJson(event.poll) + statusDao.setVoted(tuskyAccountId, event.statusId, pollString) + } + } + } + } + } + + private suspend fun removeAllByUser(tuskyAccountId: Long, accountId: String) { + timelineDao.removeAllByUser(tuskyAccountId, accountId) + notificationsDao.removeAllByUser(tuskyAccountId, accountId) + } + + fun stop() { + this.scope.cancel() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt new file mode 100644 index 0000000..490c874 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/Events.kt @@ -0,0 +1,29 @@ +package com.keylesspalace.tusky.appstore + +import com.keylesspalace.tusky.TabData +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.entity.Status + +data class StatusChangedEvent(val status: Status) : Event +data class UnfollowEvent(val accountId: String) : Event +data class BlockEvent(val accountId: String) : Event +data class MuteEvent(val accountId: String) : Event +data class StatusDeletedEvent(val statusId: String) : Event +data class StatusComposedEvent(val status: Status) : Event +data class StatusScheduledEvent(val scheduledStatus: ScheduledStatus) : Event +data class ProfileEditedEvent(val newProfileData: Account) : Event +data class PreferenceChangedEvent(val preferenceKey: String) : Event +data class MainTabsChangedEvent(val newTabs: List<TabData>) : Event +data class PollVoteEvent(val statusId: String, val poll: Poll) : Event +data class DomainMuteEvent(val instance: String) : Event +data class AnnouncementReadEvent(val announcementId: String) : Event +data class FilterUpdatedEvent(val filterContext: List<String>) : Event +data class NewNotificationsEvent( + val accountId: String, + val notifications: List<Notification> +) : Event +data class ConversationsLoadingEvent(val accountId: String) : Event +data class NotificationsLoadingEvent(val accountId: String) : Event diff --git a/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt b/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt new file mode 100644 index 0000000..511bb26 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/appstore/EventsHub.kt @@ -0,0 +1,20 @@ +package com.keylesspalace.tusky.appstore + +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow + +interface Event + +@Singleton +class EventHub @Inject constructor() { + + private val _events = MutableSharedFlow<Event>() + val events: SharedFlow<Event> = _events.asSharedFlow() + + suspend fun dispatch(event: Event) { + _events.emit(event) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt new file mode 100644 index 0000000..5ea9dc7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountActivity.kt @@ -0,0 +1,1140 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.account + +import android.animation.ArgbEvaluator +import android.content.Context +import android.content.Intent +import android.content.res.ColorStateList +import android.graphics.Color +import android.graphics.Typeface +import android.os.Bundle +import android.text.SpannableStringBuilder +import android.text.TextWatcher +import android.text.style.StyleSpan +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.activity.viewModels +import androidx.annotation.ColorInt +import androidx.annotation.DrawableRes +import androidx.annotation.Px +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.app.ActivityOptionsCompat +import androidx.core.graphics.ColorUtils +import androidx.core.view.MenuProvider +import androidx.core.view.ViewCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsCompat.Type.systemBars +import androidx.core.view.updatePadding +import androidx.core.widget.doAfterTextChanged +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.viewpager2.widget.MarginPageTransformer +import com.bumptech.glide.Glide +import com.google.android.material.R as materialR +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.chip.Chip +import com.google.android.material.color.MaterialColors +import com.google.android.material.floatingactionbutton.FloatingActionButton +import com.google.android.material.shape.MaterialShapeDrawable +import com.google.android.material.shape.ShapeAppearanceModel +import com.google.android.material.snackbar.Snackbar +import com.google.android.material.tabs.TabLayout +import com.google.android.material.tabs.TabLayoutMediator +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.EditProfileActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.StatusListActivity +import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.components.account.list.ListSelectionFragment +import com.keylesspalace.tusky.components.accountlist.AccountListActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.components.report.ReportActivity +import com.keylesspalace.tusky.databinding.ActivityAccountBinding +import com.keylesspalace.tusky.db.DraftsAlert +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Relationship +import com.keylesspalace.tusky.interfaces.AccountSelectionListener +import com.keylesspalace.tusky.interfaces.ActionButtonActivity +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.interfaces.ReselectableFragment +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.Error +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.copyToClipboard +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.getDomain +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.parseAsMastodonHtml +import com.keylesspalace.tusky.util.reduceSwipeSensitivity +import com.keylesspalace.tusky.util.setClickableText +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.view.showMuteAccountDialog +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import dagger.hilt.android.AndroidEntryPoint +import java.text.NumberFormat +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Locale +import javax.inject.Inject +import kotlin.math.abs +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class AccountActivity : BottomSheetActivity(), ActionButtonActivity, MenuProvider, LinkListener { + + @Inject + lateinit var draftsAlert: DraftsAlert + + private val viewModel: AccountViewModel by viewModels() + + private val binding: ActivityAccountBinding by viewBinding(ActivityAccountBinding::inflate) + + private lateinit var accountFieldAdapter: AccountFieldAdapter + + private var followState: FollowState = FollowState.NOT_FOLLOWING + private var blocking: Boolean = false + private var muting: Boolean = false + private var blockingDomain: Boolean = false + private var showingReblogs: Boolean = false + private var subscribing: Boolean = false + private var loadedAccount: Account? = null + + private var animateAvatar: Boolean = false + private var animateEmojis: Boolean = false + + // for scroll animation + private var oldOffset: Int = 0 + + @ColorInt + private var toolbarColor: Int = 0 + + @ColorInt + private var statusBarColorTransparent: Int = 0 + + @ColorInt + private var statusBarColorOpaque: Int = 0 + + private var avatarSize: Float = 0f + + @Px + private var titleVisibleHeight: Int = 0 + private lateinit var domain: String + + private enum class FollowState { + NOT_FOLLOWING, + FOLLOWING, + REQUESTED + } + + private lateinit var adapter: AccountPagerAdapter + + private var noteWatcher: TextWatcher? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + loadResources() + makeNotificationBarTransparent() + setContentView(binding.root) + addMenuProvider(this) + + // Obtain information to fill out the profile. + viewModel.setAccountInfo(intent.getStringExtra(KEY_ACCOUNT_ID)!!) + + animateAvatar = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + + handleWindowInsets() + setupToolbar() + setupTabs() + setupAccountViews() + setupRefreshLayout() + subscribeObservables() + + if (viewModel.isSelf) { + updateButtons() + binding.saveNoteInfo.hide() + } else { + binding.saveNoteInfo.visibility = View.INVISIBLE + } + } + + /** + * Load colors and dimensions from resources + */ + private fun loadResources() { + toolbarColor = MaterialColors.getColor(this, materialR.attr.colorSurface, Color.BLACK) + statusBarColorTransparent = getColor(R.color.transparent_statusbar_background) + statusBarColorOpaque = MaterialColors.getColor(this, materialR.attr.colorPrimaryDark, Color.BLACK) + avatarSize = resources.getDimension(R.dimen.account_activity_avatar_size) + titleVisibleHeight = resources.getDimensionPixelSize(R.dimen.account_activity_scroll_title_visible_height) + } + + /** + * Setup account widgets visibility and actions + */ + private fun setupAccountViews() { + // Initialise the default UI states. + binding.accountFloatingActionButton.hide() + binding.accountFollowButton.hide() + binding.accountMuteButton.hide() + binding.accountFollowsYouTextView.hide() + + // setup the RecyclerView for the account fields + accountFieldAdapter = AccountFieldAdapter(this, animateEmojis) + binding.accountFieldList.isNestedScrollingEnabled = false + binding.accountFieldList.layoutManager = LinearLayoutManager(this) + binding.accountFieldList.adapter = accountFieldAdapter + + val accountListClickListener = { v: View -> + val type = when (v.id) { + R.id.accountFollowers -> AccountListActivity.Type.FOLLOWERS + R.id.accountFollowing -> AccountListActivity.Type.FOLLOWS + else -> throw AssertionError() + } + val accountListIntent = AccountListActivity.newIntent(this, type, viewModel.accountId) + startActivityWithSlideInAnimation(accountListIntent) + } + binding.accountFollowers.setOnClickListener(accountListClickListener) + binding.accountFollowing.setOnClickListener(accountListClickListener) + + binding.accountStatuses.setOnClickListener { + // Make nice ripple effect on tab + binding.accountTabLayout.getTabAt(0)!!.select() + val poorTabView = (binding.accountTabLayout.getChildAt(0) as ViewGroup).getChildAt(0) + poorTabView.isPressed = true + binding.accountTabLayout.postDelayed({ poorTabView.isPressed = false }, 300) + } + + // If wellbeing mode is enabled, follow stats and posts count should be hidden + val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false) + + if (wellbeingEnabled) { + binding.accountStatuses.hide() + binding.accountFollowers.hide() + binding.accountFollowing.hide() + } + } + + /** + * Init timeline tabs + */ + private fun setupTabs() { + // Setup the tabs and timeline pager. + adapter = AccountPagerAdapter(this, viewModel.accountId) + + binding.accountFragmentViewPager.reduceSwipeSensitivity() + binding.accountFragmentViewPager.adapter = adapter + binding.accountFragmentViewPager.offscreenPageLimit = 2 + + val pageTitles = + arrayOf( + getString(R.string.title_posts), + getString(R.string.title_posts_with_replies), + getString(R.string.title_posts_pinned), + getString(R.string.title_media) + ) + + TabLayoutMediator( + binding.accountTabLayout, + binding.accountFragmentViewPager + ) { tab, position -> + tab.text = pageTitles[position] + }.attach() + + val pageMargin = resources.getDimensionPixelSize(R.dimen.tab_page_margin) + binding.accountFragmentViewPager.setPageTransformer(MarginPageTransformer(pageMargin)) + + val enableSwipeForTabs = preferences.getBoolean(PrefKeys.ENABLE_SWIPE_FOR_TABS, true) + binding.accountFragmentViewPager.isUserInputEnabled = enableSwipeForTabs + + binding.accountTabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener { + override fun onTabReselected(tab: TabLayout.Tab?) { + tab?.position?.let { position -> + (adapter.getFragment(position) as? ReselectableFragment)?.onReselect() + } + } + + override fun onTabUnselected(tab: TabLayout.Tab?) {} + + override fun onTabSelected(tab: TabLayout.Tab?) {} + }) + } + + private fun handleWindowInsets() { + ViewCompat.setOnApplyWindowInsetsListener(binding.accountCoordinatorLayout) { _, insets -> + val top = insets.getInsets(systemBars()).top + val toolbarParams = binding.accountToolbar.layoutParams as ViewGroup.MarginLayoutParams + toolbarParams.topMargin = top + + val right = insets.getInsets(systemBars()).right + val bottom = insets.getInsets(systemBars()).bottom + val left = insets.getInsets(systemBars()).left + binding.accountCoordinatorLayout.updatePadding( + right = right, + bottom = bottom, + left = left + ) + binding.swipeToRefreshLayout.setProgressViewEndTarget( + false, + top + resources.getDimensionPixelSize(R.dimen.account_swiperefresh_distance) + ) + + WindowInsetsCompat.CONSUMED + } + } + + private fun setupToolbar() { + // Setup the toolbar. + setSupportActionBar(binding.accountToolbar) + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + setDisplayShowTitleEnabled(false) + } + + val appBarElevation = resources.getDimension(R.dimen.actionbar_elevation) + + val toolbarBackground = MaterialShapeDrawable.createWithElevationOverlay( + this, + appBarElevation + ) + toolbarBackground.fillColor = ColorStateList.valueOf(Color.TRANSPARENT) + binding.accountToolbar.background = toolbarBackground + + binding.accountToolbar.setNavigationIcon(R.drawable.ic_arrow_back_with_background) + binding.accountToolbar.setOverflowIcon( + AppCompatResources.getDrawable(this, R.drawable.ic_more_with_background) + ) + + binding.accountHeaderInfoContainer.background = MaterialShapeDrawable.createWithElevationOverlay(this, appBarElevation) + + val avatarBackground = MaterialShapeDrawable.createWithElevationOverlay( + this, + appBarElevation + ).apply { + fillColor = ColorStateList.valueOf(toolbarColor) + elevation = appBarElevation + shapeAppearanceModel = ShapeAppearanceModel.builder() + .setAllCornerSizes(resources.getDimension(R.dimen.account_avatar_background_radius)) + .build() + } + binding.accountAvatarImageView.background = avatarBackground + + // Add a listener to change the toolbar icon color when it enters/exits its collapsed state. + binding.accountAppBarLayout.addOnOffsetChangedListener(object : AppBarLayout.OnOffsetChangedListener { + + override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) { + if (verticalOffset == oldOffset) { + return + } + oldOffset = verticalOffset + + if (titleVisibleHeight + verticalOffset < 0) { + supportActionBar?.setDisplayShowTitleEnabled(true) + } else { + supportActionBar?.setDisplayShowTitleEnabled(false) + } + + val scaledAvatarSize = (avatarSize + verticalOffset) / avatarSize + + binding.accountAvatarImageView.scaleX = scaledAvatarSize + binding.accountAvatarImageView.scaleY = scaledAvatarSize + + binding.accountAvatarImageView.visible(scaledAvatarSize > 0) + + val transparencyPercent = (abs(verticalOffset) / titleVisibleHeight.toFloat()).coerceAtMost( + 1f + ) + + window.statusBarColor = argbEvaluator.evaluate(transparencyPercent, statusBarColorTransparent, statusBarColorOpaque) as Int + + val evaluatedToolbarColor = argbEvaluator.evaluate( + transparencyPercent, + Color.TRANSPARENT, + toolbarColor + ) as Int + + toolbarBackground.fillColor = ColorStateList.valueOf(evaluatedToolbarColor) + + binding.swipeToRefreshLayout.isEnabled = verticalOffset == 0 + } + }) + } + + private fun makeNotificationBarTransparent() { + WindowCompat.setDecorFitsSystemWindows(window, false) + window.statusBarColor = statusBarColorTransparent + } + + /** + * Subscribe to data loaded at the view model + */ + private fun subscribeObservables() { + lifecycleScope.launch { + viewModel.accountData.collect { + if (it == null) return@collect + when (it) { + is Success -> onAccountChanged(it.data) + is Error -> { + Snackbar.make( + binding.accountCoordinatorLayout, + R.string.error_generic, + Snackbar.LENGTH_LONG + ) + .setAction(R.string.action_retry) { viewModel.refresh() } + .show() + } + is Loading -> { } + } + } + } + lifecycleScope.launch { + viewModel.relationshipData.collect { + val relation = it?.data + if (relation != null) { + onRelationshipChanged(relation) + } + + if (it is Error) { + Snackbar.make( + binding.accountCoordinatorLayout, + R.string.error_generic, + Snackbar.LENGTH_LONG + ) + .setAction(R.string.action_retry) { viewModel.refresh() } + .show() + } + } + } + lifecycleScope.launch { + viewModel.noteSaved.collect { + binding.saveNoteInfo.visible(it, View.INVISIBLE) + } + } + + // "Post failed" dialog should display in this activity + draftsAlert.observeInContext(this, true) + } + + private fun onRefresh() { + viewModel.refresh() + adapter.refreshContent() + } + + /** + * Setup swipe to refresh layout + */ + private fun setupRefreshLayout() { + binding.swipeToRefreshLayout.setOnRefreshListener { onRefresh() } + lifecycleScope.launch { + viewModel.isRefreshing.collect { isRefreshing -> + binding.swipeToRefreshLayout.isRefreshing = isRefreshing == true + } + } + } + + private fun onAccountChanged(account: Account?) { + loadedAccount = account ?: return + + val usernameFormatted = getString(R.string.post_username_format, account.username) + binding.accountUsernameTextView.text = usernameFormatted + binding.accountDisplayNameTextView.text = account.name.emojify(account.emojis, binding.accountDisplayNameTextView, animateEmojis) + + // Long press on username to copy it to clipboard + for (view in listOf(binding.accountUsernameTextView, binding.accountDisplayNameTextView)) { + view.setOnLongClickListener { + loadedAccount?.let { loadedAccount -> + copyToClipboard( + getFullUsername(loadedAccount), + getString(R.string.account_username_copied), + ) + } + true + } + } + + val emojifiedNote = account.note.parseAsMastodonHtml().emojify( + account.emojis, + binding.accountNoteTextView, + animateEmojis + ) + setClickableText(binding.accountNoteTextView, emojifiedNote, emptyList(), null, this) + + accountFieldAdapter.fields = account.fields + accountFieldAdapter.emojis = account.emojis + accountFieldAdapter.notifyDataSetChanged() + + binding.accountLockedImageView.visible(account.locked) + + updateAccountAvatar() + updateToolbar() + updateBadges() + updateMovedAccount() + updateRemoteAccount() + updateAccountJoinedDate() + updateAccountStats() + invalidateOptionsMenu() + + binding.accountMuteButton.setOnClickListener { + viewModel.unmuteAccount() + updateMuteButton() + } + } + + private fun updateBadges() { + binding.accountBadgeContainer.removeAllViews() + + val isLight = resources.getBoolean(R.bool.lightNavigationBar) + + if (loadedAccount?.bot == true) { + val badgeView = + getBadge( + getColor(R.color.tusky_grey_50), + R.drawable.ic_bot_24dp, + getString(R.string.profile_badge_bot_text), + isLight + ) + binding.accountBadgeContainer.addView(badgeView) + } + + loadedAccount?.roles?.forEach { role -> + val badgeColor = if (role.color.isNotBlank()) { + Color.parseColor(role.color) + } else { + // sometimes the color is not set for a role, in this case fall back to our default blue + getColor(R.color.tusky_blue) + } + + val sb = SpannableStringBuilder("${role.name} ${viewModel.domain}") + sb.setSpan(StyleSpan(Typeface.BOLD), 0, role.name.length, 0) + + val badgeView = getBadge(badgeColor, R.drawable.profile_badge_person_24dp, sb, isLight) + + binding.accountBadgeContainer.addView(badgeView) + } + } + + private fun updateAccountJoinedDate() { + loadedAccount?.let { account -> + try { + binding.accountDateJoined.text = resources.getString( + R.string.account_date_joined, + SimpleDateFormat("MMMM, yyyy", Locale.getDefault()).format(account.createdAt) + ) + binding.accountDateJoined.visibility = View.VISIBLE + } catch (e: ParseException) { + binding.accountDateJoined.visibility = View.GONE + } + } + } + + /** + * Load account's avatar and header image + */ + private fun updateAccountAvatar() { + loadedAccount?.let { account -> + + loadAvatar( + account.avatar, + binding.accountAvatarImageView, + resources.getDimensionPixelSize(R.dimen.avatar_radius_94dp), + animateAvatar + ) + + Glide.with(this) + .asBitmap() + .load(account.header) + .centerCrop() + .into(binding.accountHeaderImageView) + + binding.accountAvatarImageView.setOnClickListener { view -> + viewImage(view, account.avatar) + } + binding.accountHeaderImageView.setOnClickListener { view -> + viewImage(view, account.header) + } + } + } + + private fun viewImage(view: View, uri: String) { + view.transitionName = uri + startActivity( + ViewMediaActivity.newSingleImageIntent(view.context, uri), + ActivityOptionsCompat.makeSceneTransitionAnimation(this, view, uri).toBundle() + ) + } + + /** + * Update toolbar views for loaded account + */ + private fun updateToolbar() { + loadedAccount?.let { account -> + supportActionBar?.title = account.name.emojify(account.emojis, binding.accountToolbar, animateEmojis) + supportActionBar?.subtitle = String.format(getString(R.string.post_username_format), account.username) + } + } + + /** + * Update moved account info + */ + private fun updateMovedAccount() { + loadedAccount?.moved?.let { movedAccount -> + + binding.accountMovedView.show() + + binding.accountMovedView.setOnClickListener { + onViewAccount(movedAccount.id) + } + + binding.accountMovedDisplayName.text = movedAccount.name + binding.accountMovedUsername.text = getString(R.string.post_username_format, movedAccount.username) + + val avatarRadius = resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) + + loadAvatar(movedAccount.avatar, binding.accountMovedAvatar, avatarRadius, animateAvatar) + + binding.accountMovedText.text = getString(R.string.account_moved_description, movedAccount.name) + } + } + + /** + * Check is account remote and update info if so + */ + private fun updateRemoteAccount() { + loadedAccount?.let { account -> + if (account.isRemote) { + binding.accountRemoveView.show() + binding.accountRemoveView.setOnClickListener { + openLink(account.url) + } + } + } + } + + /** + * Update account stat info + */ + private fun updateAccountStats() { + loadedAccount?.let { account -> + val numberFormat = NumberFormat.getNumberInstance() + binding.accountFollowersTextView.text = numberFormat.format(account.followersCount) + binding.accountFollowingTextView.text = numberFormat.format(account.followingCount) + binding.accountStatusesTextView.text = numberFormat.format(account.statusesCount) + + binding.accountFloatingActionButton.setOnClickListener { mention() } + + binding.accountFollowButton.setOnClickListener { + val confirmFollows = preferences.getBoolean(PrefKeys.CONFIRM_FOLLOWS, false) + if (viewModel.isSelf) { + val intent = Intent(this@AccountActivity, EditProfileActivity::class.java) + startActivity(intent) + return@setOnClickListener + } + + if (blocking) { + viewModel.changeBlockState() + return@setOnClickListener + } + + when (followState) { + FollowState.NOT_FOLLOWING -> { + if (confirmFollows) { + showFollowWarningDialog() + } else { + viewModel.changeFollowState() + } + } + FollowState.REQUESTED -> { + showFollowRequestPendingDialog() + } + FollowState.FOLLOWING -> { + showUnfollowWarningDialog() + } + } + updateFollowButton() + updateSubscribeButton() + } + } + } + + private fun onRelationshipChanged(relation: Relationship) { + followState = when { + relation.following -> FollowState.FOLLOWING + relation.requested -> FollowState.REQUESTED + else -> FollowState.NOT_FOLLOWING + } + blocking = relation.blocking + muting = relation.muting + blockingDomain = relation.blockingDomain + showingReblogs = relation.showingReblogs + + // If wellbeing mode is enabled, "follows you" text should not be visible + val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_PROFILE, false) + + binding.accountFollowsYouTextView.visible(relation.followedBy && !wellbeingEnabled) + + // because subscribing is Pleroma extension, enable it __only__ when we have non-null subscribing field + // it's also now supported in Mastodon 3.3.0rc but called notifying and use different API call + if (!viewModel.isSelf && followState == FollowState.FOLLOWING && + (relation.subscribing != null || relation.notifying != null) + ) { + binding.accountSubscribeButton.show() + binding.accountSubscribeButton.setOnClickListener { + viewModel.changeSubscribingState() + } + if (relation.notifying != null) { + subscribing = relation.notifying + } else if (relation.subscribing != null) { + subscribing = relation.subscribing + } + } + + // remove the listener so it doesn't fire on non-user changes + binding.accountNoteTextInputLayout.editText?.removeTextChangedListener(noteWatcher) + + binding.accountNoteTextInputLayout.visible(relation.note != null) + binding.accountNoteTextInputLayout.editText?.setText(relation.note) + + noteWatcher = binding.accountNoteTextInputLayout.editText?.doAfterTextChanged { s -> + viewModel.noteChanged(s.toString()) + } + + updateButtons() + } + + private fun updateFollowButton() { + if (viewModel.isSelf) { + binding.accountFollowButton.setText(R.string.action_edit_own_profile) + return + } + if (blocking) { + binding.accountFollowButton.setText(R.string.action_unblock) + return + } + when (followState) { + FollowState.NOT_FOLLOWING -> { + binding.accountFollowButton.setText(R.string.action_follow) + } + FollowState.REQUESTED -> { + binding.accountFollowButton.setText(R.string.state_follow_requested) + } + FollowState.FOLLOWING -> { + binding.accountFollowButton.setText(R.string.action_unfollow) + } + } + } + + private fun updateMuteButton() { + if (muting) { + binding.accountMuteButton.setIconResource(R.drawable.ic_unmute_24dp) + } else { + binding.accountMuteButton.hide() + } + } + + private fun updateSubscribeButton() { + if (followState != FollowState.FOLLOWING) { + binding.accountSubscribeButton.hide() + } + + if (subscribing) { + binding.accountSubscribeButton.setIconResource(R.drawable.ic_notifications_active_24dp) + binding.accountSubscribeButton.contentDescription = getString(R.string.action_unsubscribe_account) + } else { + binding.accountSubscribeButton.setIconResource(R.drawable.ic_notifications_24dp) + binding.accountSubscribeButton.contentDescription = getString(R.string.action_subscribe_account) + } + } + + private fun updateButtons() { + invalidateOptionsMenu() + + if (loadedAccount?.moved == null) { + binding.accountFollowButton.show() + updateFollowButton() + updateSubscribeButton() + + if (blocking) { + binding.accountFloatingActionButton.hide() + binding.accountMuteButton.hide() + } else { + binding.accountFloatingActionButton.show() + binding.accountMuteButton.visible(muting) + updateMuteButton() + } + } else { + binding.accountFloatingActionButton.hide() + binding.accountFollowButton.hide() + binding.accountMuteButton.hide() + binding.accountSubscribeButton.hide() + } + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.account_toolbar, menu) + + val openAsItem = menu.findItem(R.id.action_open_as) + val title = openAsText + if (title == null) { + openAsItem.isVisible = false + } else { + openAsItem.title = title + } + + if (!viewModel.isSelf) { + val block = menu.findItem(R.id.action_block) + block.title = if (blocking) { + getString(R.string.action_unblock) + } else { + getString(R.string.action_block) + } + + val mute = menu.findItem(R.id.action_mute) + mute.title = if (muting) { + getString(R.string.action_unmute) + } else { + getString(R.string.action_mute) + } + + loadedAccount?.let { loadedAccount -> + val muteDomain = menu.findItem(R.id.action_mute_domain) + domain = getDomain(loadedAccount.url) + when { + // If we can't get the domain, there's no way we can mute it anyway... + // If the account is from our own domain, muting it is no-op + domain.isEmpty() || viewModel.isFromOwnDomain -> { + menu.removeItem(R.id.action_mute_domain) + } + blockingDomain -> { + muteDomain.title = getString(R.string.action_unmute_domain, domain) + } + else -> { + muteDomain.title = getString(R.string.action_mute_domain, domain) + } + } + } + + if (followState == FollowState.FOLLOWING) { + val showReblogs = menu.findItem(R.id.action_show_reblogs) + showReblogs.title = if (showingReblogs) { + getString(R.string.action_hide_reblogs) + } else { + getString(R.string.action_show_reblogs) + } + } else { + menu.removeItem(R.id.action_show_reblogs) + } + } else { + // It shouldn't be possible to block, mute or report yourself. + menu.removeItem(R.id.action_block) + menu.removeItem(R.id.action_mute) + menu.removeItem(R.id.action_mute_domain) + menu.removeItem(R.id.action_show_reblogs) + menu.removeItem(R.id.action_report) + } + + if (!viewModel.isSelf && followState != FollowState.FOLLOWING) { + menu.removeItem(R.id.action_add_or_remove_from_list) + } + + menu.findItem(R.id.action_search)?.apply { + icon = IconicsDrawable(this@AccountActivity, GoogleMaterial.Icon.gmd_search).apply { + sizeDp = 20 + colorInt = MaterialColors.getColor(binding.collapsingToolbar, android.R.attr.textColorPrimary) + } + } + } + + private fun showFollowRequestPendingDialog() { + AlertDialog.Builder(this) + .setMessage(R.string.dialog_message_cancel_follow_request) + .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun showUnfollowWarningDialog() { + AlertDialog.Builder(this) + .setMessage(R.string.dialog_unfollow_warning) + .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun showFollowWarningDialog() { + AlertDialog.Builder(this) + .setMessage(R.string.dialog_follow_warning) + .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeFollowState() } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun toggleBlockDomain(instance: String) { + if (blockingDomain) { + viewModel.unblockDomain(instance) + } else { + AlertDialog.Builder(this) + .setMessage(getString(R.string.mute_domain_warning, instance)) + .setPositiveButton( + getString(R.string.mute_domain_warning_dialog_ok) + ) { _, _ -> viewModel.blockDomain(instance) } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + } + + private fun toggleBlock() { + if (viewModel.relationshipData.value?.data?.blocking != true) { + AlertDialog.Builder(this) + .setMessage(getString(R.string.dialog_block_warning, loadedAccount?.username)) + .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.changeBlockState() } + .setNegativeButton(android.R.string.cancel, null) + .show() + } else { + viewModel.changeBlockState() + } + } + + private fun toggleMute() { + if (viewModel.relationshipData.value?.data?.muting != true) { + loadedAccount?.let { + showMuteAccountDialog( + this, + it.username + ) { notifications, duration -> + viewModel.muteAccount(notifications, duration) + } + } + } else { + viewModel.unmuteAccount() + } + } + + private fun mention() { + loadedAccount?.let { + val options = if (viewModel.isSelf) { + ComposeActivity.ComposeOptions(kind = ComposeActivity.ComposeKind.NEW) + } else { + ComposeActivity.ComposeOptions( + mentionedUsernames = setOf(it.username), + kind = ComposeActivity.ComposeKind.NEW + ) + } + val intent = ComposeActivity.startIntent(this, options) + startActivity(intent) + } + } + + override fun onViewTag(tag: String) { + val intent = StatusListActivity.newHashtagIntent(this, tag) + startActivityWithSlideInAnimation(intent) + } + + override fun onViewAccount(id: String) { + val intent = Intent(this, AccountActivity::class.java) + intent.putExtra("id", id) + startActivityWithSlideInAnimation(intent) + } + + override fun onViewUrl(url: String) { + viewUrl(url) + } + + override fun onMenuItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_open_in_web -> { + // If the account isn't loaded yet, eat the input. + loadedAccount?.let { loadedAccount -> + openLink(loadedAccount.url) + } + return true + } + R.id.action_open_as -> { + loadedAccount?.let { loadedAccount -> + showAccountChooserDialog( + item.title, + false, + object : AccountSelectionListener { + override fun onAccountSelected(account: AccountEntity) { + openAsAccount(loadedAccount.url, account) + } + } + ) + } + } + R.id.action_share_account_link -> { + // If the account isn't loaded yet, eat the input. + loadedAccount?.let { loadedAccount -> + val url = loadedAccount.url + val sendIntent = Intent() + sendIntent.action = Intent.ACTION_SEND + sendIntent.putExtra(Intent.EXTRA_TEXT, url) + sendIntent.type = "text/plain" + startActivity( + Intent.createChooser( + sendIntent, + resources.getText(R.string.send_account_link_to) + ) + ) + } + return true + } + R.id.action_share_account_username -> { + // If the account isn't loaded yet, eat the input. + loadedAccount?.let { loadedAccount -> + val fullUsername = getFullUsername(loadedAccount) + val sendIntent = Intent() + sendIntent.action = Intent.ACTION_SEND + sendIntent.putExtra(Intent.EXTRA_TEXT, fullUsername) + sendIntent.type = "text/plain" + startActivity( + Intent.createChooser( + sendIntent, + resources.getText(R.string.send_account_username_to) + ) + ) + } + return true + } + R.id.action_block -> { + toggleBlock() + return true + } + R.id.action_mute -> { + toggleMute() + return true + } + R.id.action_add_or_remove_from_list -> { + ListSelectionFragment.newInstance(viewModel.accountId).show(supportFragmentManager, null) + return true + } + R.id.action_mute_domain -> { + toggleBlockDomain(domain) + return true + } + R.id.action_show_reblogs -> { + viewModel.changeShowReblogsState() + return true + } + R.id.action_refresh -> { + binding.swipeToRefreshLayout.isRefreshing = true + onRefresh() + return true + } + R.id.action_report -> { + loadedAccount?.let { loadedAccount -> + startActivity( + ReportActivity.getIntent(this, viewModel.accountId, loadedAccount.username) + ) + } + return true + } + } + return false + } + + override fun getActionButton(): FloatingActionButton? { + return if (!blocking) { + binding.accountFloatingActionButton + } else { + null + } + } + + private fun getFullUsername(account: Account): String { + return if (account.isRemote) { + "@" + account.username + } else { + val localUsername = account.localUsername + // Note: !! here will crash if this pane is ever shown to a logged-out user. With AccountActivity this is believed to be impossible. + val domain = accountManager.activeAccount!!.domain + "@$localUsername@$domain" + } + } + + private fun getBadge( + @ColorInt baseColor: Int, + @DrawableRes icon: Int, + text: CharSequence, + isLight: Boolean + ): Chip { + val badge = Chip(this) + + // text color with maximum contrast + val textColor = if (isLight) Color.BLACK else Color.WHITE + // badge color with 50% transparency so it blends in with the theme background + val backgroundColor = Color.argb( + 128, + Color.red(baseColor), + Color.green(baseColor), + Color.blue(baseColor) + ) + // a color between the text color and the badge color + val outlineColor = ColorUtils.blendARGB(textColor, baseColor, 0.7f) + + // configure the badge + badge.text = text + badge.setTextColor(textColor) + badge.chipStrokeWidth = resources.getDimension(R.dimen.profile_badge_stroke_width) + badge.chipStrokeColor = ColorStateList.valueOf(outlineColor) + badge.setChipIconResource(icon) + badge.isChipIconVisible = true + badge.chipIconSize = resources.getDimension(R.dimen.profile_badge_icon_size) + badge.chipIconTint = ColorStateList.valueOf(outlineColor) + badge.chipBackgroundColor = ColorStateList.valueOf(backgroundColor) + + // badge isn't clickable, so disable all related behavior + badge.isClickable = false + badge.isFocusable = false + badge.setEnsureMinTouchTargetSize(false) + + // reset some chip defaults so it looks better for our badge usecase + badge.iconStartPadding = resources.getDimension(R.dimen.profile_badge_icon_start_padding) + badge.iconEndPadding = resources.getDimension(R.dimen.profile_badge_icon_end_padding) + badge.minHeight = resources.getDimensionPixelSize(R.dimen.profile_badge_min_height) + badge.chipMinHeight = resources.getDimension(R.dimen.profile_badge_min_height) + badge.updatePadding(top = 0, bottom = 0) + return badge + } + + companion object { + + private const val KEY_ACCOUNT_ID = "id" + private val argbEvaluator = ArgbEvaluator() + + @JvmStatic + fun getIntent(context: Context, accountId: String): Intent { + val intent = Intent(context, AccountActivity::class.java) + intent.putExtra(KEY_ACCOUNT_ID, accountId) + return intent + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt new file mode 100644 index 0000000..1d65628 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountFieldAdapter.kt @@ -0,0 +1,79 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.account + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemAccountFieldBinding +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.Field +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.parseAsMastodonHtml +import com.keylesspalace.tusky.util.setClickableText + +class AccountFieldAdapter( + private val linkListener: LinkListener, + private val animateEmojis: Boolean +) : RecyclerView.Adapter<BindingHolder<ItemAccountFieldBinding>>() { + + var emojis: List<Emoji> = emptyList() + var fields: List<Field> = emptyList() + + override fun getItemCount() = fields.size + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemAccountFieldBinding> { + val binding = ItemAccountFieldBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return BindingHolder(binding) + } + + override fun onBindViewHolder(holder: BindingHolder<ItemAccountFieldBinding>, position: Int) { + val field = fields[position] + val nameTextView = holder.binding.accountFieldName + val valueTextView = holder.binding.accountFieldValue + + val emojifiedName = field.name.emojify(emojis, nameTextView, animateEmojis) + nameTextView.text = emojifiedName + + val emojifiedValue = field.value.parseAsMastodonHtml().emojify( + emojis, + valueTextView, + animateEmojis + ) + setClickableText(valueTextView, emojifiedValue, emptyList(), null, linkListener) + + if (field.verifiedAt != null) { + valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds( + 0, + 0, + R.drawable.ic_check_circle, + 0 + ) + } else { + valueTextView.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountPagerAdapter.kt new file mode 100644 index 0000000..f41448e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountPagerAdapter.kt @@ -0,0 +1,59 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.account + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import com.keylesspalace.tusky.components.account.media.AccountMediaFragment +import com.keylesspalace.tusky.components.timeline.TimelineFragment +import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel +import com.keylesspalace.tusky.interfaces.RefreshableFragment +import com.keylesspalace.tusky.util.CustomFragmentStateAdapter + +class AccountPagerAdapter( + activity: FragmentActivity, + private val accountId: String +) : CustomFragmentStateAdapter(activity) { + + override fun getItemCount() = TAB_COUNT + + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER, accountId, false) + 1 -> TimelineFragment.newInstance( + TimelineViewModel.Kind.USER_WITH_REPLIES, + accountId, + false + ) + 2 -> TimelineFragment.newInstance(TimelineViewModel.Kind.USER_PINNED, accountId, false) + 3 -> AccountMediaFragment.newInstance(accountId) + else -> throw AssertionError("Page $position is out of AccountPagerAdapter bounds") + } + } + + fun refreshContent() { + for (i in 0 until TAB_COUNT) { + val fragment = getFragment(i) + if (fragment != null && fragment is RefreshableFragment) { + (fragment as RefreshableFragment).refreshContent() + } + } + } + + companion object { + private const val TAB_COUNT = 4 + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt new file mode 100644 index 0000000..8716ee8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/AccountViewModel.kt @@ -0,0 +1,352 @@ +package com.keylesspalace.tusky.components.account + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold +import com.keylesspalace.tusky.appstore.BlockEvent +import com.keylesspalace.tusky.appstore.DomainMuteEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.MuteEvent +import com.keylesspalace.tusky.appstore.ProfileEditedEvent +import com.keylesspalace.tusky.appstore.UnfollowEvent +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Relationship +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.Error +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.Resource +import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.getDomain +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Job +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +@HiltViewModel +class AccountViewModel @Inject constructor( + private val mastodonApi: MastodonApi, + private val eventHub: EventHub, + accountManager: AccountManager +) : ViewModel() { + + private val _accountData = MutableStateFlow(null as Resource<Account>?) + val accountData: StateFlow<Resource<Account>?> = _accountData.asStateFlow() + + private val _relationshipData = MutableStateFlow(null as Resource<Relationship>?) + val relationshipData: StateFlow<Resource<Relationship>?> = _relationshipData.asStateFlow() + + private val _noteSaved = MutableStateFlow(false) + val noteSaved: StateFlow<Boolean> = _noteSaved.asStateFlow() + + private val _isRefreshing = MutableSharedFlow<Boolean>(1, onBufferOverflow = BufferOverflow.DROP_OLDEST) + val isRefreshing: SharedFlow<Boolean> = _isRefreshing.asSharedFlow() + + private var isDataLoading = false + + lateinit var accountId: String + var isSelf = false + + /** the domain of the viewed account **/ + var domain = "" + + /** True if the viewed account has the same domain as the active account */ + var isFromOwnDomain = false + + private var noteUpdateJob: Job? = null + + private val activeAccount = accountManager.activeAccount!! + + init { + viewModelScope.launch { + eventHub.events.collect { event -> + if (event is ProfileEditedEvent && event.newProfileData.id == _accountData.value?.data?.id) { + _accountData.value = Success(event.newProfileData) + } + } + } + } + + private fun obtainAccount(reload: Boolean = false) { + if (_accountData.value == null || reload) { + isDataLoading = true + _accountData.value = Loading() + + viewModelScope.launch { + mastodonApi.account(accountId) + .fold( + { account -> + domain = getDomain(account.url) + isFromOwnDomain = domain == activeAccount.domain + + _accountData.value = Success(account) + isDataLoading = false + _isRefreshing.emit(false) + }, + { t -> + Log.w(TAG, "failed obtaining account", t) + _accountData.value = Error(cause = t) + isDataLoading = false + _isRefreshing.emit(false) + } + ) + } + } + } + + private fun obtainRelationship(reload: Boolean = false) { + if (_relationshipData.value == null || reload) { + _relationshipData.value = Loading() + + viewModelScope.launch { + mastodonApi.relationships(listOf(accountId)) + .fold( + { relationships -> + _relationshipData.value = + if (relationships.isNotEmpty()) { + Success( + relationships[0] + ) + } else { + Error() + } + }, + { t -> + Log.w(TAG, "failed obtaining relationships", t) + _relationshipData.value = Error(cause = t) + } + ) + } + } + } + + fun changeFollowState() { + val relationship = _relationshipData.value?.data + if (relationship?.following == true || relationship?.requested == true) { + changeRelationship(RelationShipAction.UNFOLLOW) + } else { + changeRelationship(RelationShipAction.FOLLOW) + } + } + + fun changeBlockState() { + if (_relationshipData.value?.data?.blocking == true) { + changeRelationship(RelationShipAction.UNBLOCK) + } else { + changeRelationship(RelationShipAction.BLOCK) + } + } + + fun muteAccount(notifications: Boolean, duration: Int?) { + changeRelationship(RelationShipAction.MUTE, notifications, duration) + } + + fun unmuteAccount() { + changeRelationship(RelationShipAction.UNMUTE) + } + + fun changeSubscribingState() { + val relationship = _relationshipData.value?.data + if (relationship?.notifying == true || // Mastodon 3.3.0rc1 + relationship?.subscribing == true // Pleroma + ) { + changeRelationship(RelationShipAction.UNSUBSCRIBE) + } else { + changeRelationship(RelationShipAction.SUBSCRIBE) + } + } + + fun blockDomain(instance: String) { + viewModelScope.launch { + mastodonApi.blockDomain(instance).fold({ + eventHub.dispatch(DomainMuteEvent(instance)) + val relation = _relationshipData.value?.data + if (relation != null) { + _relationshipData.value = Success(relation.copy(blockingDomain = true)) + } + }, { e -> + Log.e(TAG, "Error muting $instance", e) + }) + } + } + + fun unblockDomain(instance: String) { + viewModelScope.launch { + mastodonApi.unblockDomain(instance).fold({ + val relation = _relationshipData.value?.data + if (relation != null) { + _relationshipData.value = Success(relation.copy(blockingDomain = false)) + } + }, { e -> + Log.e(TAG, "Error unmuting $instance", e) + }) + } + } + + fun changeShowReblogsState() { + if (_relationshipData.value?.data?.showingReblogs == true) { + changeRelationship(RelationShipAction.FOLLOW, false) + } else { + changeRelationship(RelationShipAction.FOLLOW, true) + } + } + + /** + * @param parameter showReblogs if RelationShipAction.FOLLOW, notifications if MUTE + */ + private fun changeRelationship( + relationshipAction: RelationShipAction, + parameter: Boolean? = null, + duration: Int? = null + ) = viewModelScope.launch { + val relation = _relationshipData.value?.data + val account = _accountData.value?.data + val isMastodon = _relationshipData.value?.data?.notifying != null + + if (relation != null && account != null) { + // optimistically post new state for faster response + + val newRelation = when (relationshipAction) { + RelationShipAction.FOLLOW -> { + if (account.locked) { + relation.copy(requested = true) + } else { + relation.copy(following = true) + } + } + RelationShipAction.UNFOLLOW -> relation.copy(following = false) + RelationShipAction.BLOCK -> relation.copy(blocking = true) + RelationShipAction.UNBLOCK -> relation.copy(blocking = false) + RelationShipAction.MUTE -> relation.copy(muting = true) + RelationShipAction.UNMUTE -> relation.copy(muting = false) + RelationShipAction.SUBSCRIBE -> { + if (isMastodon) { + relation.copy(notifying = true) + } else { + relation.copy(subscribing = true) + } + } + RelationShipAction.UNSUBSCRIBE -> { + if (isMastodon) { + relation.copy(notifying = false) + } else { + relation.copy(subscribing = false) + } + } + } + _relationshipData.value = Loading(newRelation) + } + + val relationshipCall = when (relationshipAction) { + RelationShipAction.FOLLOW -> mastodonApi.followAccount( + accountId, + showReblogs = parameter ?: true + ) + RelationShipAction.UNFOLLOW -> mastodonApi.unfollowAccount(accountId) + RelationShipAction.BLOCK -> mastodonApi.blockAccount(accountId) + RelationShipAction.UNBLOCK -> mastodonApi.unblockAccount(accountId) + RelationShipAction.MUTE -> mastodonApi.muteAccount( + accountId, + parameter ?: true, + duration + ) + RelationShipAction.UNMUTE -> mastodonApi.unmuteAccount(accountId) + RelationShipAction.SUBSCRIBE -> { + if (isMastodon) { + mastodonApi.followAccount(accountId, notify = true) + } else { + mastodonApi.subscribeAccount(accountId) + } + } + RelationShipAction.UNSUBSCRIBE -> { + if (isMastodon) { + mastodonApi.followAccount(accountId, notify = false) + } else { + mastodonApi.unsubscribeAccount(accountId) + } + } + } + + relationshipCall.fold( + { relationship -> + _relationshipData.value = Success(relationship) + + when (relationshipAction) { + RelationShipAction.UNFOLLOW -> eventHub.dispatch(UnfollowEvent(accountId)) + RelationShipAction.BLOCK -> eventHub.dispatch(BlockEvent(accountId)) + RelationShipAction.MUTE -> eventHub.dispatch(MuteEvent(accountId)) + else -> { } + } + }, + { t -> + Log.w(TAG, "failed loading relationship", t) + _relationshipData.value = Error(relation, cause = t) + } + ) + } + + fun noteChanged(newNote: String) { + _noteSaved.value = false + noteUpdateJob?.cancel() + noteUpdateJob = viewModelScope.launch { + delay(1500) + mastodonApi.updateAccountNote(accountId, newNote) + .fold( + { + _noteSaved.value = true + delay(4000) + _noteSaved.value = false + }, + { t -> + Log.w(TAG, "Error updating note", t) + } + ) + } + } + + fun refresh() { + reload(true) + } + + private fun reload(isReload: Boolean = false) { + if (isDataLoading) { + return + } + accountId.let { + obtainAccount(isReload) + if (!isSelf) { + obtainRelationship(isReload) + } + } + } + + fun setAccountInfo(accountId: String) { + this.accountId = accountId + this.isSelf = activeAccount.accountId == accountId + reload(false) + } + + enum class RelationShipAction { + FOLLOW, + UNFOLLOW, + BLOCK, + UNBLOCK, + MUTE, + UNMUTE, + SUBSCRIBE, + UNSUBSCRIBE + } + + companion object { + const val TAG = "AccountViewModel" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListSelectionFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListSelectionFragment.kt new file mode 100644 index 0000000..9d1b4e4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListSelectionFragment.kt @@ -0,0 +1,245 @@ +/* Copyright Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.components.account.list + +import android.app.Dialog +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.ListsActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.FragmentListsListBinding +import com.keylesspalace.tusky.databinding.ItemListBinding +import com.keylesspalace.tusky.entity.MastoList +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.visible +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class ListSelectionFragment : DialogFragment() { + + interface ListSelectionListener { + fun onListSelected(list: MastoList) + } + + private val viewModel: ListsForAccountViewModel by viewModels() + + private var selectListener: ListSelectionListener? = null + private var accountId: String? = null + + override fun onAttach(context: Context) { + super.onAttach(context) + selectListener = context as? ListSelectionListener + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle) + accountId = requireArguments().getString(ARG_ACCOUNT_ID) + } + + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val context = requireContext() + + val binding = FragmentListsListBinding.inflate(layoutInflater) + val adapter = Adapter() + binding.listsView.adapter = adapter + + val dialogBuilder = AlertDialog.Builder(context) + .setView(binding.root) + .setTitle(R.string.select_list_title) + .setNeutralButton(R.string.select_list_manage) { _, _ -> + val listIntent = Intent(context, ListsActivity::class.java) + startActivity(listIntent) + } + .setNegativeButton(if (accountId != null) R.string.button_done else android.R.string.cancel, null) + + val dialog = dialogBuilder.create() + + val showProgressBarJob = getProgressBarJob(binding.progressBar, 500) + showProgressBarJob.start() + + // TODO change this to a (single) LoadState like elsewhere? + lifecycleScope.launch { + viewModel.states.collectLatest { states -> + binding.progressBar.hide() + showProgressBarJob.cancel() + if (states.isEmpty()) { + binding.messageView.show() + binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.no_lists) + } else { + binding.listsView.show() + adapter.submitList(states) + } + } + } + + lifecycleScope.launch { + viewModel.loadError.collectLatest { error -> + Log.e(TAG, "failed to load lists", error) + binding.progressBar.hide() + showProgressBarJob.cancel() + binding.listsView.hide() + binding.messageView.apply { + show() + setup(error) { load(binding) } + } + } + } + + lifecycleScope.launch { + viewModel.actionError.collectLatest { error -> + when (error.type) { + ActionError.Type.ADD -> { + Snackbar.make( + binding.root, + R.string.failed_to_add_to_list, + Snackbar.LENGTH_LONG + ) + .setAction(R.string.action_retry) { + viewModel.addAccountToList(accountId!!, error.listId) + } + .show() + } + ActionError.Type.REMOVE -> { + Snackbar.make( + binding.root, + R.string.failed_to_remove_from_list, + Snackbar.LENGTH_LONG + ) + .setAction(R.string.action_retry) { + viewModel.removeAccountFromList(accountId!!, error.listId) + } + .show() + } + } + } + } + + lifecycleScope.launch { + load(binding) + } + + return dialog + } + + private fun getProgressBarJob(progressView: View, delayMs: Long) = this.lifecycleScope.launch( + start = CoroutineStart.LAZY + ) { + try { + delay(delayMs) + progressView.show() + awaitCancellation() + } finally { + progressView.hide() + } + } + + private fun load(binding: FragmentListsListBinding) { + binding.progressBar.show() + binding.listsView.hide() + binding.messageView.hide() + viewModel.load(accountId) + } + + private object Differ : DiffUtil.ItemCallback<AccountListState>() { + override fun areItemsTheSame( + oldItem: AccountListState, + newItem: AccountListState + ): Boolean { + return oldItem.list.id == newItem.list.id + } + + override fun areContentsTheSame( + oldItem: AccountListState, + newItem: AccountListState + ): Boolean { + return oldItem == newItem + } + } + + inner class Adapter : + ListAdapter<AccountListState, BindingHolder<ItemListBinding>>(Differ) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemListBinding> { + return BindingHolder(ItemListBinding.inflate(LayoutInflater.from(parent.context), parent, false)) + } + + override fun onBindViewHolder(holder: BindingHolder<ItemListBinding>, position: Int) { + val item = getItem(position) + holder.binding.listName.text = item.list.title + accountId?.let { accountId -> + holder.binding.addButton.apply { + visible(!item.includesAccount) + setOnClickListener { + viewModel.addAccountToList(accountId, item.list.id) + } + } + holder.binding.removeButton.apply { + visible(item.includesAccount) + setOnClickListener { + viewModel.removeAccountFromList(accountId, item.list.id) + } + } + } + + holder.itemView.setOnClickListener { + selectListener?.onListSelected(item.list) + + accountId?.let { accountId -> + if (item.includesAccount) { + viewModel.removeAccountFromList(accountId, item.list.id) + } else { + viewModel.addAccountToList(accountId, item.list.id) + } + } + } + } + } + + companion object { + private const val TAG = "ListsListFragment" + private const val ARG_ACCOUNT_ID = "accountId" + + fun newInstance(accountId: String?): ListSelectionFragment { + val args = Bundle().apply { + putString(ARG_ACCOUNT_ID, accountId) + } + return ListSelectionFragment().apply { arguments = args } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt new file mode 100644 index 0000000..692aa2d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/list/ListsForAccountViewModel.kt @@ -0,0 +1,135 @@ +/* Copyright 2022 kyori19 + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.components.account.list + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.getOrThrow +import at.connyduck.calladapter.networkresult.onFailure +import at.connyduck.calladapter.networkresult.onSuccess +import at.connyduck.calladapter.networkresult.runCatching +import com.keylesspalace.tusky.entity.MastoList +import com.keylesspalace.tusky.network.MastodonApi +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch + +data class AccountListState( + val list: MastoList, + val includesAccount: Boolean +) + +data class ActionError( + val error: Throwable, + val type: Type, + val listId: String +) : Throwable(error) { + enum class Type { + ADD, + REMOVE + } +} + +@HiltViewModel +@OptIn(ExperimentalCoroutinesApi::class) +class ListsForAccountViewModel @Inject constructor( + private val mastodonApi: MastodonApi +) : ViewModel() { + + private val _states = MutableSharedFlow<List<AccountListState>>(1) + val states: SharedFlow<List<AccountListState>> = _states.asSharedFlow() + + private val _loadError = MutableSharedFlow<Throwable>(1) + val loadError: SharedFlow<Throwable> = _loadError.asSharedFlow() + + private val _actionError = MutableSharedFlow<ActionError>(1) + val actionError: SharedFlow<ActionError> = _actionError.asSharedFlow() + + fun load(accountId: String?) { + _loadError.resetReplayCache() + viewModelScope.launch { + runCatching { + val all = mastodonApi.getLists().getOrThrow() + var includes: List<MastoList> = emptyList() + if (accountId != null) { + includes = mastodonApi.getListsIncludesAccount(accountId).getOrThrow() + } + + _states.emit( + all.map { listState -> + AccountListState( + list = listState, + includesAccount = includes.any { it.id == listState.id } + ) + } + ) + } + .onFailure { + _loadError.emit(it) + } + } + } + + // TODO there is no "progress" visible for these + + fun addAccountToList(accountId: String, listId: String) { + _actionError.resetReplayCache() + viewModelScope.launch { + mastodonApi.addAccountToList(listId, listOf(accountId)) + .onSuccess { + _states.emit( + _states.first().map { state -> + if (state.list.id == listId) { + state.copy(includesAccount = true) + } else { + state + } + } + ) + } + .onFailure { + _actionError.emit(ActionError(it, ActionError.Type.ADD, listId)) + } + } + } + + fun removeAccountFromList(accountId: String, listId: String) { + _actionError.resetReplayCache() + viewModelScope.launch { + mastodonApi.deleteAccountFromList(listId, listOf(accountId)) + .onSuccess { + _states.emit( + _states.first().map { state -> + if (state.list.id == listId) { + state.copy(includesAccount = false) + } else { + state + } + } + ) + } + .onFailure { + _actionError.emit(ActionError(it, ActionError.Type.REMOVE, listId)) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt new file mode 100644 index 0000000..fb87827 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaFragment.kt @@ -0,0 +1,231 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.account.media + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.MenuProvider +import androidx.core.view.ViewCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState +import androidx.recyclerview.widget.GridLayoutManager +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.databinding.FragmentTimelineBinding +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.interfaces.RefreshableFragment +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.openLink +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +/** + * Fragment with multiple columns of media previews for the specified account. + */ +@AndroidEntryPoint +class AccountMediaFragment : + Fragment(R.layout.fragment_timeline), + RefreshableFragment, + MenuProvider { + + @Inject + lateinit var accountManager: AccountManager + + @Inject + lateinit var preferences: SharedPreferences + + private val binding by viewBinding(FragmentTimelineBinding::bind) + + private val viewModel: AccountMediaViewModel by viewModels() + + private var adapter: AccountMediaGridAdapter? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + viewModel.accountId = arguments?.getString(ACCOUNT_ID_ARG)!! + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) + + val useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true) + + val adapter = AccountMediaGridAdapter( + useBlurhash = useBlurhash, + context = view.context, + onAttachmentClickListener = ::onAttachmentClick + ) + this.adapter = adapter + + val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count) + val imageSpacing = view.context.resources.getDimensionPixelSize( + R.dimen.profile_media_spacing + ) + + binding.recyclerView.addItemDecoration( + GridSpacingItemDecoration(columnCount, imageSpacing, 0) + ) + + binding.recyclerView.layoutManager = GridLayoutManager(view.context, columnCount) + binding.recyclerView.adapter = adapter + + binding.swipeRefreshLayout.isEnabled = false + binding.swipeRefreshLayout.setOnRefreshListener { refreshContent() } + + binding.statusView.visibility = View.GONE + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.media.collectLatest { pagingData -> + adapter.submitData(pagingData) + } + } + + adapter.addLoadStateListener { loadState -> + binding.statusView.hide() + binding.progressBar.hide() + + if (loadState.refresh != LoadState.Loading && loadState.source.refresh != LoadState.Loading) { + binding.swipeRefreshLayout.isRefreshing = false + } + + if (adapter.itemCount == 0) { + when (loadState.refresh) { + is LoadState.NotLoading -> { + if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) { + binding.statusView.show() + binding.statusView.setup( + R.drawable.elephant_friend_empty, + R.string.message_empty, + null + ) + } + } + is LoadState.Error -> { + binding.statusView.show() + binding.statusView.setup((loadState.refresh as LoadState.Error).error) + } + is LoadState.Loading -> { + binding.progressBar.show() + } + } + } + } + } + + override fun onDestroyView() { + // Clear the adapter to prevent leaking the View + adapter = null + super.onDestroyView() + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.fragment_account_media, menu) + menu.findItem(R.id.action_refresh)?.apply { + icon = IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_refresh).apply { + sizeDp = 20 + colorInt = MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary) + } + } + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_refresh -> { + binding.swipeRefreshLayout.isRefreshing = true + refreshContent() + true + } + else -> false + } + } + + private fun onAttachmentClick(selected: AttachmentViewData, view: View) { + if (!selected.isRevealed) { + viewModel.revealAttachment(selected) + return + } + val attachmentsFromSameStatus = viewModel.attachmentData.filter { attachmentViewData -> + attachmentViewData.statusId == selected.statusId + } + val currentIndex = attachmentsFromSameStatus.indexOf(selected) + + when (selected.attachment.type) { + Attachment.Type.IMAGE, + Attachment.Type.GIFV, + Attachment.Type.VIDEO, + Attachment.Type.AUDIO -> { + val intent = ViewMediaActivity.newIntent( + context, + attachmentsFromSameStatus, + currentIndex + ) + if (activity != null) { + val url = selected.attachment.url + ViewCompat.setTransitionName(view, url) + val options = ActivityOptionsCompat.makeSceneTransitionAnimation( + requireActivity(), + view, + url + ) + startActivity(intent, options.toBundle()) + } else { + startActivity(intent) + } + } + Attachment.Type.UNKNOWN -> { + context?.openLink(selected.attachment.url) + } + } + } + + override fun refreshContent() { + adapter?.refresh() + } + + companion object { + + fun newInstance(accountId: String): AccountMediaFragment { + val fragment = AccountMediaFragment() + val args = Bundle(1) + args.putString(ACCOUNT_ID_ARG, accountId) + fragment.arguments = args + return fragment + } + + private const val ACCOUNT_ID_ARG = "account_id" + private const val TAG = "AccountMediaFragment" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt new file mode 100644 index 0000000..6135464 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaGridAdapter.kt @@ -0,0 +1,148 @@ +package com.keylesspalace.tusky.components.account.media + +import android.content.Context +import android.graphics.Color +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.content.res.AppCompatResources +import androidx.appcompat.widget.TooltipCompat +import androidx.core.view.setPadding +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import com.bumptech.glide.Glide +import com.google.android.material.R as materialR +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemAccountMediaBinding +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.decodeBlurHash +import com.keylesspalace.tusky.util.getFormattedDescription +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import kotlin.random.Random + +class AccountMediaGridAdapter( + private val useBlurhash: Boolean, + context: Context, + private val onAttachmentClickListener: (AttachmentViewData, View) -> Unit +) : PagingDataAdapter<AttachmentViewData, BindingHolder<ItemAccountMediaBinding>>( + object : DiffUtil.ItemCallback<AttachmentViewData>() { + override fun areItemsTheSame( + oldItem: AttachmentViewData, + newItem: AttachmentViewData + ): Boolean { + return oldItem.attachment.id == newItem.attachment.id + } + + override fun areContentsTheSame( + oldItem: AttachmentViewData, + newItem: AttachmentViewData + ): Boolean { + return oldItem == newItem + } + } +) { + + private val baseItemBackgroundColor = MaterialColors.getColor( + context, + materialR.attr.colorSurface, + Color.BLACK + ) + private val videoIndicator = AppCompatResources.getDrawable( + context, + R.drawable.ic_play_indicator + ) + private val mediaHiddenDrawable = AppCompatResources.getDrawable( + context, + R.drawable.ic_hide_media_24dp + ) + + private val itemBgBaseHSV = FloatArray(3) + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemAccountMediaBinding> { + val binding = ItemAccountMediaBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + Color.colorToHSV(baseItemBackgroundColor, itemBgBaseHSV) + itemBgBaseHSV[2] = itemBgBaseHSV[2] + Random.nextFloat() / 3f - 1f / 6f + binding.root.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV)) + return BindingHolder(binding) + } + + override fun onBindViewHolder(holder: BindingHolder<ItemAccountMediaBinding>, position: Int) { + val context = holder.binding.root.context + getItem(position)?.let { item -> + + val imageView = holder.binding.accountMediaImageView + val overlay = holder.binding.accountMediaImageViewOverlay + + val blurhash = item.attachment.blurhash + val placeholder = if (useBlurhash && blurhash != null) { + decodeBlurHash(context, blurhash) + } else { + null + } + + if (item.attachment.type == Attachment.Type.AUDIO) { + overlay.hide() + + imageView.setPadding( + context.resources.getDimensionPixelSize( + R.dimen.profile_media_audio_icon_padding + ) + ) + + Glide.with(imageView) + .load(R.drawable.ic_music_box_preview_24dp) + .centerInside() + .into(imageView) + + imageView.contentDescription = item.attachment.getFormattedDescription(context) + } else if (item.sensitive && !item.isRevealed) { + overlay.show() + overlay.setImageDrawable(mediaHiddenDrawable) + + imageView.setPadding(0) + + Glide.with(imageView) + .load(placeholder) + .centerInside() + .into(imageView) + + imageView.contentDescription = imageView.context.getString(R.string.post_media_hidden_title) + } else { + if (item.attachment.type == Attachment.Type.VIDEO || item.attachment.type == Attachment.Type.GIFV) { + overlay.show() + overlay.setImageDrawable(videoIndicator) + } else { + overlay.hide() + } + + imageView.setPadding(0) + + Glide.with(imageView) + .asBitmap() + .load(item.attachment.previewUrl) + .placeholder(placeholder) + .centerInside() + .into(imageView) + + imageView.contentDescription = item.attachment.getFormattedDescription(context) + } + + holder.binding.root.setOnClickListener { + onAttachmentClickListener(item, imageView) + } + + TooltipCompat.setTooltipText(holder.binding.root, imageView.contentDescription) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaPagingSource.kt new file mode 100644 index 0000000..0ed67cf --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaPagingSource.kt @@ -0,0 +1,36 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.account.media + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.keylesspalace.tusky.viewdata.AttachmentViewData + +class AccountMediaPagingSource( + private val viewModel: AccountMediaViewModel +) : PagingSource<String, AttachmentViewData>() { + + override fun getRefreshKey(state: PagingState<String, AttachmentViewData>): String? = null + + override suspend fun load(params: LoadParams<String>): LoadResult<String, AttachmentViewData> { + return if (params is LoadParams.Refresh) { + val list = viewModel.attachmentData.toList() + LoadResult.Page(list, null, list.lastOrNull()?.statusId) + } else { + LoadResult.Page(emptyList(), null, null) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaRemoteMediator.kt new file mode 100644 index 0000000..dd8dae7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaRemoteMediator.kt @@ -0,0 +1,87 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.account.media + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import com.keylesspalace.tusky.components.timeline.util.ifExpected +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import retrofit2.HttpException + +@OptIn(ExperimentalPagingApi::class) +class AccountMediaRemoteMediator( + private val api: MastodonApi, + private val activeAccount: AccountEntity, + private val viewModel: AccountMediaViewModel +) : RemoteMediator<String, AttachmentViewData>() { + override suspend fun load( + loadType: LoadType, + state: PagingState<String, AttachmentViewData> + ): MediatorResult { + try { + val statusResponse = when (loadType) { + LoadType.REFRESH -> { + api.accountStatuses(viewModel.accountId, onlyMedia = true) + } + LoadType.PREPEND -> { + return MediatorResult.Success(endOfPaginationReached = true) + } + LoadType.APPEND -> { + val maxId = state.lastItemOrNull()?.statusId + if (maxId != null) { + api.accountStatuses(viewModel.accountId, maxId = maxId, onlyMedia = true) + } else { + return MediatorResult.Success(endOfPaginationReached = false) + } + } + } + + val statuses = statusResponse.body() + if (!statusResponse.isSuccessful || statuses == null) { + return MediatorResult.Error(HttpException(statusResponse)) + } + + val attachments = statuses.flatMap { status -> + status.attachments.map { attachment -> + AttachmentViewData( + attachment = attachment, + statusId = status.id, + statusUrl = status.url.orEmpty(), + sensitive = status.sensitive, + isRevealed = activeAccount.alwaysShowSensitiveMedia || !status.sensitive + ) + } + } + + if (loadType == LoadType.REFRESH) { + viewModel.attachmentData.clear() + } + + viewModel.attachmentData.addAll(attachments) + + viewModel.currentSource?.invalidate() + return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty()) + } catch (e: Exception) { + return ifExpected(e) { + MediatorResult.Error(e) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaViewModel.kt new file mode 100644 index 0000000..0f90772 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/AccountMediaViewModel.kt @@ -0,0 +1,70 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.account.media + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject + +@HiltViewModel +class AccountMediaViewModel @Inject constructor( + accountManager: AccountManager, + api: MastodonApi +) : ViewModel() { + + lateinit var accountId: String + + val attachmentData: MutableList<AttachmentViewData> = mutableListOf() + + var currentSource: AccountMediaPagingSource? = null + + val activeAccount = accountManager.activeAccount!! + + @OptIn(ExperimentalPagingApi::class) + val media = Pager( + config = PagingConfig( + pageSize = LOAD_AT_ONCE, + prefetchDistance = LOAD_AT_ONCE * 2 + ), + pagingSourceFactory = { + AccountMediaPagingSource( + viewModel = this + ).also { source -> + currentSource = source + } + }, + remoteMediator = AccountMediaRemoteMediator(api, activeAccount, this) + ).flow + .cachedIn(viewModelScope) + + fun revealAttachment(viewData: AttachmentViewData) { + val position = attachmentData.indexOfFirst { oldViewData -> oldViewData.id == viewData.id } + attachmentData[position] = viewData.copy(isRevealed = true) + currentSource?.invalidate() + } + + companion object { + private const val LOAD_AT_ONCE = 30 + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/GridSpacingItemDecoration.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/GridSpacingItemDecoration.kt new file mode 100644 index 0000000..34ad159 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/GridSpacingItemDecoration.kt @@ -0,0 +1,47 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.account.media + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.ItemDecoration + +class GridSpacingItemDecoration( + private val spanCount: Int, + private val spacing: Int, + private val topOffset: Int +) : ItemDecoration() { + + override fun getItemOffsets( + outRect: Rect, + view: View, + parent: RecyclerView, + state: RecyclerView.State + ) { + val position = parent.getChildAdapterPosition(view) // item position + if (position < topOffset) return + + val column = (position - topOffset) % spanCount // item column + + outRect.left = column * spacing / spanCount // column * ((1f / spanCount) * spacing) + outRect.right = + spacing - (column + 1) * spacing / spanCount // spacing - (column + 1) * ((1f / spanCount) * spacing) + if (position - topOffset >= spanCount) { + outRect.top = spacing // item top + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/account/media/SquareImageView.kt b/app/src/main/java/com/keylesspalace/tusky/components/account/media/SquareImageView.kt new file mode 100644 index 0000000..b696bfc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/account/media/SquareImageView.kt @@ -0,0 +1,24 @@ +package com.keylesspalace.tusky.components.account.media + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatImageView + +/** + * Created by charlag on 26/10/2017. + */ + +class SquareImageView : AppCompatImageView { + constructor(context: Context) : super(context) + + constructor(context: Context, attributes: AttributeSet) : super(context, attributes) + + constructor(context: Context, attributes: AttributeSet, defStyleAttr: Int) : + super(context, attributes, defStyleAttr) + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + val width = measuredWidth + setMeasuredDimension(width, width) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListActivity.kt new file mode 100644 index 0000000..b8d972c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListActivity.kt @@ -0,0 +1,80 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.accountlist + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.commit +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ActivityAccountListBinding +import com.keylesspalace.tusky.util.getSerializableExtraCompat +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class AccountListActivity : BottomSheetActivity() { + + enum class Type { + FOLLOWS, + FOLLOWERS, + BLOCKS, + MUTES, + FOLLOW_REQUESTS, + REBLOGGED, + FAVOURITED + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val binding = ActivityAccountListBinding.inflate(layoutInflater) + setContentView(binding.root) + + val type = intent.getSerializableExtraCompat<Type>(EXTRA_TYPE)!! + val id: String? = intent.getStringExtra(EXTRA_ID) + + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.apply { + when (type) { + Type.BLOCKS -> setTitle(R.string.title_blocks) + Type.MUTES -> setTitle(R.string.title_mutes) + Type.FOLLOW_REQUESTS -> setTitle(R.string.title_follow_requests) + Type.FOLLOWERS -> setTitle(R.string.title_followers) + Type.FOLLOWS -> setTitle(R.string.title_follows) + Type.REBLOGGED -> setTitle(R.string.title_reblogged_by) + Type.FAVOURITED -> setTitle(R.string.title_favourited_by) + } + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + supportFragmentManager.commit { + replace(R.id.fragment_container, AccountListFragment.newInstance(type, id)) + } + } + + companion object { + private const val EXTRA_TYPE = "type" + private const val EXTRA_ID = "id" + + fun newIntent(context: Context, type: Type, id: String? = null): Intent { + return Intent(context, AccountListActivity::class.java).apply { + putExtra(EXTRA_TYPE, type) + putExtra(EXTRA_ID, id) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt new file mode 100644 index 0000000..a463ef7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/AccountListFragment.kt @@ -0,0 +1,441 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.accountlist + +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.ConcatAdapter +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator +import at.connyduck.calladapter.networkresult.fold +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.PostLookupFallbackBehavior +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.StatusListActivity +import com.keylesspalace.tusky.components.account.AccountActivity +import com.keylesspalace.tusky.components.accountlist.AccountListActivity.Type +import com.keylesspalace.tusky.components.accountlist.adapter.AccountAdapter +import com.keylesspalace.tusky.components.accountlist.adapter.BlocksAdapter +import com.keylesspalace.tusky.components.accountlist.adapter.FollowAdapter +import com.keylesspalace.tusky.components.accountlist.adapter.FollowRequestsAdapter +import com.keylesspalace.tusky.components.accountlist.adapter.FollowRequestsHeaderAdapter +import com.keylesspalace.tusky.components.accountlist.adapter.MutesAdapter +import com.keylesspalace.tusky.databinding.FragmentAccountListBinding +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Relationship +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.HttpHeaderLink +import com.keylesspalace.tusky.util.getSerializableCompat +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.view.EndlessOnScrollListener +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.launch +import retrofit2.Response + +@AndroidEntryPoint +class AccountListFragment : + Fragment(R.layout.fragment_account_list), + AccountActionListener, + LinkListener { + + @Inject + lateinit var api: MastodonApi + + @Inject + lateinit var accountManager: AccountManager + + @Inject + lateinit var preferences: SharedPreferences + + private val binding by viewBinding(FragmentAccountListBinding::bind) + + private lateinit var type: Type + private var id: String? = null + + private var adapter: AccountAdapter<*>? = null + private var fetching = false + private var bottomId: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + type = requireArguments().getSerializableCompat(ARG_TYPE)!! + id = requireArguments().getString(ARG_ID) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.recyclerView.setHasFixedSize(true) + val layoutManager = LinearLayoutManager(view.context) + binding.recyclerView.layoutManager = layoutManager + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + binding.recyclerView.addItemDecoration( + DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL) + ) + + val animateAvatar = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) + val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + val showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true) + + val activeAccount = accountManager.activeAccount!! + + val adapter = when (type) { + Type.BLOCKS -> BlocksAdapter(this, animateAvatar, animateEmojis, showBotOverlay) + Type.MUTES -> MutesAdapter(this, animateAvatar, animateEmojis, showBotOverlay) + Type.FOLLOW_REQUESTS -> { + val headerAdapter = FollowRequestsHeaderAdapter( + instanceName = activeAccount.domain, + accountLocked = activeAccount.locked + ) + val followRequestsAdapter = + FollowRequestsAdapter(this, this, animateAvatar, animateEmojis, showBotOverlay) + binding.recyclerView.adapter = ConcatAdapter(headerAdapter, followRequestsAdapter) + followRequestsAdapter + } + else -> FollowAdapter(this, animateAvatar, animateEmojis, showBotOverlay) + } + this.adapter = adapter + if (binding.recyclerView.adapter == null) { + binding.recyclerView.adapter = adapter + } + + val scrollListener = object : EndlessOnScrollListener(layoutManager) { + override fun onLoadMore(totalItemsCount: Int, view: RecyclerView) { + if (bottomId == null) { + return + } + fetchAccounts(adapter, bottomId) + } + } + + binding.recyclerView.addOnScrollListener(scrollListener) + + binding.swipeRefreshLayout.setOnRefreshListener { fetchAccounts(adapter) } + + fetchAccounts(adapter) + } + + override fun onDestroyView() { + // Clear the adapter to prevent leaking the View + adapter = null + super.onDestroyView() + } + + override fun onViewTag(tag: String) { + activity?.startActivityWithSlideInAnimation( + StatusListActivity.newHashtagIntent(requireContext(), tag) + ) + } + + override fun onViewAccount(id: String) { + activity?.startActivityWithSlideInAnimation(AccountActivity.getIntent(requireContext(), id)) + } + + override fun onViewUrl(url: String) { + (activity as BottomSheetActivity?)?.viewUrl(url, PostLookupFallbackBehavior.OPEN_IN_BROWSER) + } + + override fun onMute(mute: Boolean, id: String, position: Int, notifications: Boolean) { + viewLifecycleOwner.lifecycleScope.launch { + try { + if (!mute) { + api.unmuteAccount(id) + } else { + api.muteAccount(id, notifications) + } + onMuteSuccess(mute, id, position, notifications) + } catch (_: Throwable) { + onMuteFailure(mute, id, notifications) + } + } + } + + private fun onMuteSuccess(muted: Boolean, id: String, position: Int, notifications: Boolean) { + val mutesAdapter = adapter as MutesAdapter + if (muted) { + mutesAdapter.updateMutingNotifications(id, notifications, position) + return + } + val unmutedUser = mutesAdapter.removeItem(position) + + if (unmutedUser != null) { + Snackbar.make(binding.recyclerView, R.string.confirmation_unmuted, Snackbar.LENGTH_LONG) + .setAction(R.string.action_undo) { + mutesAdapter.addItem(unmutedUser, position) + onMute(true, id, position, notifications) + } + .show() + } + } + + private fun onMuteFailure(mute: Boolean, accountId: String, notifications: Boolean) { + val verb = if (mute) { + if (notifications) { + "mute (notifications = true)" + } else { + "mute (notifications = false)" + } + } else { + "unmute" + } + Log.e(TAG, "Failed to $verb account id $accountId") + } + + override fun onBlock(block: Boolean, id: String, position: Int) { + viewLifecycleOwner.lifecycleScope.launch { + try { + if (!block) { + api.unblockAccount(id) + } else { + api.blockAccount(id) + } + onBlockSuccess(block, id, position) + } catch (_: Throwable) { + onBlockFailure(block, id) + } + } + } + + private fun onBlockSuccess(blocked: Boolean, id: String, position: Int) { + if (blocked) { + return + } + val blocksAdapter = adapter as BlocksAdapter + val unblockedUser = blocksAdapter.removeItem(position) + + if (unblockedUser != null) { + Snackbar.make( + binding.recyclerView, + R.string.confirmation_unblocked, + Snackbar.LENGTH_LONG + ) + .setAction(R.string.action_undo) { + blocksAdapter.addItem(unblockedUser, position) + onBlock(true, id, position) + } + .show() + } + } + + private fun onBlockFailure(block: Boolean, accountId: String) { + val verb = if (block) { + "block" + } else { + "unblock" + } + Log.e(TAG, "Failed to $verb account accountId $accountId") + } + + override fun onRespondToFollowRequest(accept: Boolean, id: String, position: Int) { + viewLifecycleOwner.lifecycleScope.launch { + if (accept) { + api.authorizeFollowRequest(id) + } else { + api.rejectFollowRequest(id) + }.fold( + onSuccess = { + onRespondToFollowRequestSuccess(position) + }, + onFailure = { throwable -> + val verb = if (accept) { + "accept" + } else { + "reject" + } + Log.e(TAG, "Failed to $verb account id $id.", throwable) + } + ) + } + } + + private fun onRespondToFollowRequestSuccess(position: Int) { + val followRequestsAdapter = adapter as FollowRequestsAdapter + followRequestsAdapter.removeItem(position) + } + + private suspend fun getFetchCallByListType(fromId: String?): Response<List<TimelineAccount>> { + return when (type) { + Type.FOLLOWS -> { + val accountId = requireId(type, id) + api.accountFollowing(accountId, fromId) + } + Type.FOLLOWERS -> { + val accountId = requireId(type, id) + api.accountFollowers(accountId, fromId) + } + Type.BLOCKS -> api.blocks(fromId) + Type.MUTES -> api.mutes(fromId) + Type.FOLLOW_REQUESTS -> api.followRequests(fromId) + Type.REBLOGGED -> { + val statusId = requireId(type, id) + api.statusRebloggedBy(statusId, fromId) + } + Type.FAVOURITED -> { + val statusId = requireId(type, id) + api.statusFavouritedBy(statusId, fromId) + } + } + } + + private fun requireId(type: Type, id: String?): String { + return requireNotNull(id) { "id must not be null for type " + type.name } + } + + private fun fetchAccounts(adapter: AccountAdapter<*>, fromId: String? = null) { + if (fetching) { + return + } + fetching = true + binding.swipeRefreshLayout.isRefreshing = true + + if (fromId != null) { + binding.recyclerView.post { adapter.setBottomLoading(true) } + } + + viewLifecycleOwner.lifecycleScope.launch { + try { + val response = getFetchCallByListType(fromId) + + if (!response.isSuccessful) { + onFetchAccountsFailure(adapter, Exception(response.message())) + return@launch + } + + val accountList = response.body() + + if (accountList == null) { + onFetchAccountsFailure(adapter, Exception(response.message())) + return@launch + } + + val linkHeader = response.headers()["Link"] + onFetchAccountsSuccess(adapter, accountList, linkHeader) + } catch (exception: Exception) { + if (exception is CancellationException) { + // Scope is cancelled, probably because the fragment is destroyed. + // We must not touch any views anymore, so rethrow the exception. + // (CancellationException in a cancelled scope is normal and will be ignored) + throw exception + } + onFetchAccountsFailure(adapter, exception) + } + } + } + + private fun onFetchAccountsSuccess( + adapter: AccountAdapter<*>, + accounts: List<TimelineAccount>, + linkHeader: String? + ) { + adapter.setBottomLoading(false) + binding.swipeRefreshLayout.isRefreshing = false + + val links = HttpHeaderLink.parse(linkHeader) + val next = HttpHeaderLink.findByRelationType(links, "next") + val fromId = next?.uri?.getQueryParameter("max_id") + + if (adapter.itemCount > 0) { + adapter.addItems(accounts) + } else { + adapter.update(accounts) + } + + if (adapter is MutesAdapter) { + fetchRelationships(adapter, accounts.map { it.id }) + } + + bottomId = fromId + + fetching = false + + if (adapter.itemCount == 0) { + binding.messageView.show() + binding.messageView.setup( + R.drawable.elephant_friend_empty, + R.string.message_empty, + null + ) + } else { + binding.messageView.hide() + } + } + + private fun fetchRelationships(mutesAdapter: MutesAdapter, ids: List<String>) { + viewLifecycleOwner.lifecycleScope.launch { + api.relationships(ids) + .fold( + onSuccess = { relationships -> + onFetchRelationshipsSuccess(mutesAdapter, relationships) + }, + onFailure = { throwable -> + Log.e(TAG, "Fetch failure for relationships of accounts: $ids", throwable) + } + ) + } + } + + private fun onFetchRelationshipsSuccess( + mutesAdapter: MutesAdapter, + relationships: List<Relationship> + ) { + val mutingNotificationsMap = HashMap<String, Boolean>() + relationships.map { mutingNotificationsMap.put(it.id, it.mutingNotifications) } + mutesAdapter.updateMutingNotificationsMap(mutingNotificationsMap) + } + + private fun onFetchAccountsFailure(adapter: AccountAdapter<*>, throwable: Throwable) { + fetching = false + binding.swipeRefreshLayout.isRefreshing = false + Log.e(TAG, "Fetch failure", throwable) + + if (adapter.itemCount == 0) { + binding.messageView.show() + binding.messageView.setup(throwable) { + binding.messageView.hide() + this.fetchAccounts(adapter, null) + } + } + } + + companion object { + private const val TAG = "AccountList" // logging tag + private const val ARG_TYPE = "type" + private const val ARG_ID = "id" + + fun newInstance(type: Type, id: String? = null): AccountListFragment { + return AccountListFragment().apply { + arguments = Bundle(3).apply { + putSerializable(ARG_TYPE, type) + putString(ARG_ID, id) + } + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt new file mode 100644 index 0000000..ac327ac --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/AccountAdapter.kt @@ -0,0 +1,124 @@ +/* Copyright 2021 Tusky Contributors. + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ +package com.keylesspalace.tusky.components.accountlist.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.databinding.ItemFooterBinding +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.removeDuplicatesTo + +/** Generic adapter with bottom loading indicator. */ +abstract class AccountAdapter<AVH : RecyclerView.ViewHolder> internal constructor( + protected val accountActionListener: AccountActionListener, + protected val animateAvatar: Boolean, + protected val animateEmojis: Boolean, + protected val showBotOverlay: Boolean +) : RecyclerView.Adapter<RecyclerView.ViewHolder?>() { + + protected var accountList: MutableList<TimelineAccount> = mutableListOf() + private var bottomLoading: Boolean = false + + override fun getItemCount(): Int { + return accountList.size + if (bottomLoading) 1 else 0 + } + + abstract fun createAccountViewHolder(parent: ViewGroup): AVH + + abstract fun onBindAccountViewHolder(viewHolder: AVH, position: Int) + + final override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (getItemViewType(position) == VIEW_TYPE_ACCOUNT) { + @Suppress("UNCHECKED_CAST") + this.onBindAccountViewHolder(holder as AVH, position) + } + } + + final override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): RecyclerView.ViewHolder { + return when (viewType) { + VIEW_TYPE_ACCOUNT -> this.createAccountViewHolder(parent) + VIEW_TYPE_FOOTER -> this.createFooterViewHolder(parent) + else -> error("Unknown item type: $viewType") + } + } + + private fun createFooterViewHolder(parent: ViewGroup): RecyclerView.ViewHolder { + val binding = ItemFooterBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) + } + + override fun getItemViewType(position: Int): Int { + return if (position == accountList.size && bottomLoading) { + VIEW_TYPE_FOOTER + } else { + VIEW_TYPE_ACCOUNT + } + } + + fun update(newAccounts: List<TimelineAccount>) { + accountList = newAccounts.removeDuplicatesTo(ArrayList()) + notifyDataSetChanged() + } + + fun addItems(newAccounts: List<TimelineAccount>) { + val end = accountList.size + val last = accountList[end - 1] + if (newAccounts.none { it.id == last.id }) { + accountList.addAll(newAccounts) + notifyItemRangeInserted(end, newAccounts.size) + } + } + + fun setBottomLoading(loading: Boolean) { + val wasLoading = bottomLoading + if (wasLoading == loading) { + return + } + bottomLoading = loading + if (loading) { + notifyItemInserted(accountList.size) + } else { + notifyItemRemoved(accountList.size) + } + } + + fun removeItem(position: Int): TimelineAccount? { + if (position < 0 || position >= accountList.size) { + return null + } + val account = accountList.removeAt(position) + notifyItemRemoved(position) + return account + } + + fun addItem(account: TimelineAccount, position: Int) { + if (position < 0 || position > accountList.size) { + return + } + accountList.add(position, account) + notifyItemInserted(position) + } + + companion object { + const val VIEW_TYPE_ACCOUNT = 0 + const val VIEW_TYPE_FOOTER = 1 + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/BlocksAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/BlocksAdapter.kt new file mode 100644 index 0000000..c1132e7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/BlocksAdapter.kt @@ -0,0 +1,79 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.accountlist.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemBlockedUserBinding +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.visible + +/** Displays a list of blocked accounts. */ +class BlocksAdapter( + accountActionListener: AccountActionListener, + animateAvatar: Boolean, + animateEmojis: Boolean, + showBotOverlay: Boolean +) : AccountAdapter<BindingHolder<ItemBlockedUserBinding>>( + accountActionListener = accountActionListener, + animateAvatar = animateAvatar, + animateEmojis = animateEmojis, + showBotOverlay = showBotOverlay +) { + + override fun createAccountViewHolder(parent: ViewGroup): BindingHolder<ItemBlockedUserBinding> { + val binding = ItemBlockedUserBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return BindingHolder(binding) + } + + override fun onBindAccountViewHolder( + viewHolder: BindingHolder<ItemBlockedUserBinding>, + position: Int + ) { + val account = accountList[position] + val binding = viewHolder.binding + val context = binding.root.context + + val emojifiedName = account.name.emojify( + account.emojis, + binding.blockedUserDisplayName, + animateEmojis + ) + binding.blockedUserDisplayName.text = emojifiedName + val formattedUsername = context.getString(R.string.post_username_format, account.username) + binding.blockedUserUsername.text = formattedUsername + + val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) + loadAvatar(account.avatar, binding.blockedUserAvatar, avatarRadius, animateAvatar) + + binding.blockedUserBotBadge.visible(showBotOverlay && account.bot) + + binding.blockedUserUnblock.setOnClickListener { + accountActionListener.onBlock(false, account.id, position) + } + binding.root.setOnClickListener { + accountActionListener.onViewAccount(account.id) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowAdapter.kt new file mode 100644 index 0000000..87b6248 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowAdapter.kt @@ -0,0 +1,51 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.accountlist.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.keylesspalace.tusky.adapter.AccountViewHolder +import com.keylesspalace.tusky.databinding.ItemAccountBinding +import com.keylesspalace.tusky.interfaces.AccountActionListener + +/** Displays either a follows or following list. */ +class FollowAdapter( + accountActionListener: AccountActionListener, + animateAvatar: Boolean, + animateEmojis: Boolean, + showBotOverlay: Boolean +) : AccountAdapter<AccountViewHolder>( + accountActionListener = accountActionListener, + animateAvatar = animateAvatar, + animateEmojis = animateEmojis, + showBotOverlay = showBotOverlay +) { + + override fun createAccountViewHolder(parent: ViewGroup): AccountViewHolder { + val binding = ItemAccountBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return AccountViewHolder(binding) + } + + override fun onBindAccountViewHolder(viewHolder: AccountViewHolder, position: Int) { + viewHolder.setupWithAccount( + accountList[position], + animateAvatar, + animateEmojis, + showBotOverlay + ) + viewHolder.setupActionListener(accountActionListener) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowRequestsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowRequestsAdapter.kt new file mode 100644 index 0000000..fc860e5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowRequestsAdapter.kt @@ -0,0 +1,62 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.accountlist.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import com.keylesspalace.tusky.adapter.FollowRequestViewHolder +import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.interfaces.LinkListener + +/** Displays a list of follow requests with accept/reject buttons. */ +class FollowRequestsAdapter( + accountActionListener: AccountActionListener, + private val linkListener: LinkListener, + animateAvatar: Boolean, + animateEmojis: Boolean, + showBotOverlay: Boolean +) : AccountAdapter<FollowRequestViewHolder>( + accountActionListener = accountActionListener, + animateAvatar = animateAvatar, + animateEmojis = animateEmojis, + showBotOverlay = showBotOverlay +) { + + override fun createAccountViewHolder(parent: ViewGroup): FollowRequestViewHolder { + val binding = ItemFollowRequestBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return FollowRequestViewHolder( + binding, + accountActionListener, + linkListener, + showHeader = false + ) + } + + override fun onBindAccountViewHolder(viewHolder: FollowRequestViewHolder, position: Int) { + viewHolder.setupWithAccount( + account = accountList[position], + animateAvatar = animateAvatar, + animateEmojis = animateEmojis, + showBotOverlay = showBotOverlay + ) + viewHolder.setupActionListener(accountActionListener, accountList[position].id) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowRequestsHeaderAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowRequestsHeaderAdapter.kt new file mode 100644 index 0000000..069e799 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/FollowRequestsHeaderAdapter.kt @@ -0,0 +1,50 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.accountlist.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemFollowRequestsHeaderBinding +import com.keylesspalace.tusky.util.BindingHolder + +class FollowRequestsHeaderAdapter( + private val instanceName: String, + private val accountLocked: Boolean +) : RecyclerView.Adapter<BindingHolder<ItemFollowRequestsHeaderBinding>>() { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemFollowRequestsHeaderBinding> { + val binding = ItemFollowRequestsHeaderBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return BindingHolder(binding) + } + + override fun onBindViewHolder( + viewHolder: BindingHolder<ItemFollowRequestsHeaderBinding>, + position: Int + ) { + viewHolder.binding.root.text = viewHolder.binding.root.context.getString(R.string.follow_requests_info, instanceName) + } + + override fun getItemCount() = if (accountLocked) 0 else 1 +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/MutesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/MutesAdapter.kt new file mode 100644 index 0000000..d685730 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/accountlist/adapter/MutesAdapter.kt @@ -0,0 +1,120 @@ +/* Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.accountlist.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.ViewCompat +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemMutedUserBinding +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.visible + +/** Displays a list of muted accounts with mute/unmute account button and mute/unmute notifications switch */ +class MutesAdapter( + accountActionListener: AccountActionListener, + animateAvatar: Boolean, + animateEmojis: Boolean, + showBotOverlay: Boolean +) : AccountAdapter<BindingHolder<ItemMutedUserBinding>>( + accountActionListener = accountActionListener, + animateAvatar = animateAvatar, + animateEmojis = animateEmojis, + showBotOverlay = showBotOverlay +) { + + private val mutingNotificationsMap = HashMap<String, Boolean>() + + override fun createAccountViewHolder(parent: ViewGroup): BindingHolder<ItemMutedUserBinding> { + val binding = ItemMutedUserBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return BindingHolder(binding) + } + + override fun onBindAccountViewHolder( + viewHolder: BindingHolder<ItemMutedUserBinding>, + position: Int + ) { + val account = accountList[position] + val binding = viewHolder.binding + val context = binding.root.context + + val mutingNotifications = mutingNotificationsMap[account.id] + + val emojifiedName = account.name.emojify( + account.emojis, + binding.mutedUserDisplayName, + animateEmojis + ) + binding.mutedUserDisplayName.text = emojifiedName + + val formattedUsername = context.getString(R.string.post_username_format, account.username) + binding.mutedUserUsername.text = formattedUsername + + val avatarRadius = context.resources.getDimensionPixelSize(R.dimen.avatar_radius_48dp) + loadAvatar(account.avatar, binding.mutedUserAvatar, avatarRadius, animateAvatar) + + binding.mutedUserBotBadge.visible(showBotOverlay && account.bot) + + val unmuteString = context.getString(R.string.action_unmute_desc, formattedUsername) + binding.mutedUserUnmute.contentDescription = unmuteString + ViewCompat.setTooltipText(binding.mutedUserUnmute, unmuteString) + + binding.mutedUserMuteNotifications.setOnCheckedChangeListener(null) + + binding.mutedUserMuteNotifications.isChecked = if (mutingNotifications == null) { + binding.mutedUserMuteNotifications.isEnabled = false + true + } else { + binding.mutedUserMuteNotifications.isEnabled = true + mutingNotifications + } + + binding.mutedUserUnmute.setOnClickListener { + accountActionListener.onMute( + false, + account.id, + viewHolder.bindingAdapterPosition, + false + ) + } + binding.mutedUserMuteNotifications.setOnCheckedChangeListener { _, isChecked -> + accountActionListener.onMute( + true, + account.id, + viewHolder.bindingAdapterPosition, + isChecked + ) + } + binding.root.setOnClickListener { accountActionListener.onViewAccount(account.id) } + } + + fun updateMutingNotifications(id: String, mutingNotifications: Boolean, position: Int) { + mutingNotificationsMap[id] = mutingNotifications + notifyItemChanged(position) + } + + fun updateMutingNotificationsMap(newMutingNotificationsMap: HashMap<String, Boolean>) { + mutingNotificationsMap.putAll(newMutingNotificationsMap) + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt new file mode 100644 index 0000000..2cdb1db --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementAdapter.kt @@ -0,0 +1,176 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.announcements + +import android.annotation.SuppressLint +import android.graphics.drawable.Drawable +import android.os.Build +import android.text.SpannableString +import android.view.ContextThemeWrapper +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.size +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.Target +import com.google.android.material.chip.Chip +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemAnnouncementBinding +import com.keylesspalace.tusky.entity.Announcement +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.util.AbsoluteTimeFormatter +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.EmojiSpan +import com.keylesspalace.tusky.util.clearEmojiTargets +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.parseAsMastodonHtml +import com.keylesspalace.tusky.util.setClickableText +import com.keylesspalace.tusky.util.setEmojiTargets +import com.keylesspalace.tusky.util.visible + +interface AnnouncementActionListener : LinkListener { + fun openReactionPicker(announcementId: String, target: View) + fun addReaction(announcementId: String, name: String) + fun removeReaction(announcementId: String, name: String) +} + +class AnnouncementAdapter( + private var items: List<Announcement> = emptyList(), + private val listener: AnnouncementActionListener, + private val wellbeingEnabled: Boolean = false, + private val animateEmojis: Boolean = false +) : RecyclerView.Adapter<BindingHolder<ItemAnnouncementBinding>>() { + + private val absoluteTimeFormatter = AbsoluteTimeFormatter() + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemAnnouncementBinding> { + val binding = ItemAnnouncementBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return BindingHolder(binding) + } + + @SuppressLint("SetTextI18n") + override fun onBindViewHolder(holder: BindingHolder<ItemAnnouncementBinding>, position: Int) { + val item = items[position] + + holder.binding.announcementDate.text = absoluteTimeFormatter.format(item.publishedAt, false) + + val text = holder.binding.text + val chips = holder.binding.chipGroup + val addReactionChip = holder.binding.addReactionChip + + val emojifiedText: CharSequence = item.content.parseAsMastodonHtml().emojify( + item.emojis, + text, + animateEmojis + ) + + setClickableText(text, emojifiedText, item.mentions, item.tags, listener) + + // If wellbeing mode is enabled, announcement badge counts should not be shown. + if (wellbeingEnabled) { + // Since reactions are not visible in wellbeing mode, + // we shouldn't be able to add any ourselves. + addReactionChip.visibility = View.GONE + return + } + + // hide button if announcement badge limit is already reached + addReactionChip.visible(item.reactions.size < 8) + + val requestManager = Glide.with(chips) + + chips.clearEmojiTargets() + val targets = ArrayList<Target<Drawable>>(item.reactions.size) + + item.reactions.forEachIndexed { i, reaction -> + ( + chips.getChildAt(i)?.takeUnless { it.id == R.id.addReactionChip } as Chip? + ?: Chip(ContextThemeWrapper(chips.context, com.google.android.material.R.style.Widget_MaterialComponents_Chip_Choice)).apply { + isCheckable = true + checkedIcon = null + chips.addView(this, i) + } + ) + .apply { + if (reaction.url == null) { + this.text = "${reaction.name} ${reaction.count}" + } else { + // we set the EmojiSpan on a space, because otherwise the Chip won't have the right size + // https://github.com/tuskyapp/Tusky/issues/2308 + val spannable = SpannableString(" ${reaction.count}") + val span = EmojiSpan(this) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + span.contentDescription = reaction.name + } + val target = span.createGlideTarget(this, animateEmojis) + spannable.setSpan(span, 0, 1, 0) + requestManager + .asDrawable() + .load( + if (animateEmojis) { + reaction.url + } else { + reaction.staticUrl + } + ) + .into(target) + targets.add(target) + this.text = spannable + } + + isChecked = reaction.me + + setOnClickListener { + if (reaction.me) { + listener.removeReaction(item.id, reaction.name) + } else { + listener.addReaction(item.id, reaction.name) + } + } + } + } + + while (chips.size - 1 > item.reactions.size) { + chips.removeViewAt(item.reactions.size) + } + + // Store Glide targets for later cancellation + chips.setEmojiTargets(targets) + + addReactionChip.setOnClickListener { + listener.openReactionPicker(item.id, it) + } + } + + override fun onViewRecycled(holder: BindingHolder<ItemAnnouncementBinding>) { + holder.binding.chipGroup.clearEmojiTargets() + } + + override fun getItemCount() = items.size + + fun updateList(items: List<Announcement>) { + this.items = items + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt new file mode 100644 index 0000000..f777dba --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsActivity.kt @@ -0,0 +1,213 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.announcements + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.widget.PopupWindow +import androidx.activity.viewModels +import androidx.core.view.MenuProvider +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.StatusListActivity +import com.keylesspalace.tusky.adapter.EmojiAdapter +import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener +import com.keylesspalace.tusky.databinding.ActivityAnnouncementsBinding +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.Error +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import com.keylesspalace.tusky.util.unsafeLazy +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.view.EmojiPicker +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class AnnouncementsActivity : + BottomSheetActivity(), + AnnouncementActionListener, + OnEmojiSelectedListener, + MenuProvider { + + private val viewModel: AnnouncementsViewModel by viewModels() + + private val binding by viewBinding(ActivityAnnouncementsBinding::inflate) + + private lateinit var adapter: AnnouncementAdapter + + private val picker by unsafeLazy { EmojiPicker(this) } + private val pickerDialog by unsafeLazy { + PopupWindow(this) + .apply { + contentView = picker + isFocusable = true + setOnDismissListener { + currentAnnouncementId = null + } + } + } + private var currentAnnouncementId: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + addMenuProvider(this) + + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.apply { + title = getString(R.string.title_announcements) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + binding.swipeRefreshLayout.setOnRefreshListener(this::refreshAnnouncements) + + binding.announcementsList.setHasFixedSize(true) + binding.announcementsList.layoutManager = LinearLayoutManager(this) + val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) + binding.announcementsList.addItemDecoration(divider) + + val wellbeingEnabled = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false) + val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + + adapter = AnnouncementAdapter(emptyList(), this, wellbeingEnabled, animateEmojis) + + binding.announcementsList.adapter = adapter + + lifecycleScope.launch { + viewModel.announcements.collect { + if (it == null) return@collect + when (it) { + is Success -> { + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false + if (it.data.isNullOrEmpty()) { + binding.errorMessageView.setup( + R.drawable.elephant_friend_empty, + R.string.no_announcements + ) + binding.errorMessageView.show() + } else { + binding.errorMessageView.hide() + } + adapter.updateList(it.data ?: listOf()) + } + is Loading -> { + binding.errorMessageView.hide() + } + is Error -> { + binding.progressBar.hide() + binding.swipeRefreshLayout.isRefreshing = false + binding.errorMessageView.setup( + R.drawable.errorphant_error, + R.string.error_generic + ) { + refreshAnnouncements() + } + binding.errorMessageView.show() + } + } + } + } + + lifecycleScope.launch { + viewModel.emoji.collect { + picker.adapter = EmojiAdapter(it, this@AnnouncementsActivity, animateEmojis) + } + } + + viewModel.load() + binding.progressBar.show() + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.activity_announcements, menu) + menu.findItem(R.id.action_search)?.apply { + icon = IconicsDrawable(this@AnnouncementsActivity, GoogleMaterial.Icon.gmd_search).apply { + sizeDp = 20 + colorInt = MaterialColors.getColor(binding.includedToolbar.toolbar, android.R.attr.textColorPrimary) + } + } + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_refresh -> { + binding.swipeRefreshLayout.isRefreshing = true + refreshAnnouncements() + true + } + else -> false + } + } + + private fun refreshAnnouncements() { + viewModel.load() + binding.swipeRefreshLayout.isRefreshing = true + } + + override fun openReactionPicker(announcementId: String, target: View) { + currentAnnouncementId = announcementId + pickerDialog.showAsDropDown(target) + } + + override fun onEmojiSelected(shortcode: String) { + viewModel.addReaction(currentAnnouncementId!!, shortcode) + pickerDialog.dismiss() + } + + override fun addReaction(announcementId: String, name: String) { + viewModel.addReaction(announcementId, name) + } + + override fun removeReaction(announcementId: String, name: String) { + viewModel.removeReaction(announcementId, name) + } + + override fun onViewTag(tag: String) { + val intent = StatusListActivity.newHashtagIntent(this, tag) + startActivityWithSlideInAnimation(intent) + } + + override fun onViewAccount(id: String) { + viewAccount(id) + } + + override fun onViewUrl(url: String) { + viewUrl(url) + } + + companion object { + fun newIntent(context: Context) = Intent(context, AnnouncementsActivity::class.java) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt new file mode 100644 index 0000000..3834c5d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/announcements/AnnouncementsViewModel.kt @@ -0,0 +1,181 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.announcements + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold +import com.keylesspalace.tusky.appstore.AnnouncementReadEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository +import com.keylesspalace.tusky.entity.Announcement +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.Error +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.Resource +import com.keylesspalace.tusky.util.Success +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +@HiltViewModel +class AnnouncementsViewModel @Inject constructor( + private val instanceInfoRepo: InstanceInfoRepository, + private val mastodonApi: MastodonApi, + private val eventHub: EventHub +) : ViewModel() { + + private val _announcements = MutableStateFlow(null as Resource<List<Announcement>>?) + val announcements: StateFlow<Resource<List<Announcement>>?> = _announcements.asStateFlow() + + private val _emoji = MutableStateFlow(emptyList<Emoji>()) + val emoji: StateFlow<List<Emoji>> = _emoji.asStateFlow() + + init { + viewModelScope.launch { + _emoji.value = instanceInfoRepo.getEmojis() + } + } + + fun load() { + viewModelScope.launch { + _announcements.value = Loading() + mastodonApi.listAnnouncements() + .fold( + { + _announcements.value = Success(it) + it.filter { announcement -> !announcement.read } + .forEach { announcement -> + mastodonApi.dismissAnnouncement(announcement.id) + .fold( + { + eventHub.dispatch( + AnnouncementReadEvent(announcement.id) + ) + }, + { throwable -> + Log.d( + TAG, + "Failed to mark announcement as read.", + throwable + ) + } + ) + } + }, + { + _announcements.value = Error(cause = it) + } + ) + } + } + + fun addReaction(announcementId: String, name: String) { + viewModelScope.launch { + mastodonApi.addAnnouncementReaction(announcementId, name) + .fold( + { + _announcements.value = + Success( + announcements.value?.data?.map { announcement -> + if (announcement.id == announcementId) { + announcement.copy( + reactions = if (announcement.reactions.find { reaction -> reaction.name == name } != null) { + announcement.reactions.map { reaction -> + if (reaction.name == name) { + reaction.copy( + count = reaction.count + 1, + me = true + ) + } else { + reaction + } + } + } else { + listOf( + *announcement.reactions.toTypedArray(), + emoji.value.find { emoji -> emoji.shortcode == name }!!.run { + Announcement.Reaction( + name, + 1, + true, + url, + staticUrl + ) + } + ) + } + ) + } else { + announcement + } + } + ) + }, + { + Log.w(TAG, "Failed to add reaction to the announcement.", it) + } + ) + } + } + + fun removeReaction(announcementId: String, name: String) { + viewModelScope.launch { + mastodonApi.removeAnnouncementReaction(announcementId, name) + .fold( + { + _announcements.value = + Success( + announcements.value!!.data!!.map { announcement -> + if (announcement.id == announcementId) { + announcement.copy( + reactions = announcement.reactions.mapNotNull { reaction -> + if (reaction.name == name) { + if (reaction.count > 1) { + reaction.copy( + count = reaction.count - 1, + me = false + ) + } else { + null + } + } else { + reaction + } + } + ) + } else { + announcement + } + } + ) + }, + { + Log.w(TAG, "Failed to remove reaction from the announcement.", it) + } + ) + } + } + + companion object { + private const val TAG = "AnnouncementsViewModel" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt new file mode 100644 index 0000000..d391bfc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -0,0 +1,1608 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.compose + +import android.Manifest +import android.app.ProgressDialog +import android.content.ClipData +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.graphics.Bitmap +import android.icu.text.BreakIterator +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Parcelable +import android.provider.MediaStore +import android.text.Spanned +import android.text.style.URLSpan +import android.util.Log +import android.view.KeyEvent +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.AdapterView +import android.widget.ImageButton +import android.widget.LinearLayout +import android.widget.PopupMenu +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.annotation.AttrRes +import androidx.annotation.ColorInt +import androidx.annotation.StringRes +import androidx.annotation.VisibleForTesting +import androidx.appcompat.app.AlertDialog +import androidx.core.content.FileProvider +import androidx.core.content.res.use +import androidx.core.view.ContentInfoCompat +import androidx.core.view.OnReceiveContentListener +import androidx.core.view.isGone +import androidx.core.view.isVisible +import androidx.core.widget.doAfterTextChanged +import androidx.core.widget.doOnTextChanged +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.transition.TransitionManager +import com.canhub.cropper.CropImage +import com.canhub.cropper.CropImageContract +import com.canhub.cropper.options +import com.google.android.material.R as materialR +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback +import com.google.android.material.color.MaterialColors +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.EmojiAdapter +import com.keylesspalace.tusky.adapter.LocaleAdapter +import com.keylesspalace.tusky.adapter.OnEmojiSelectedListener +import com.keylesspalace.tusky.components.compose.ComposeViewModel.ConfirmationKind +import com.keylesspalace.tusky.components.compose.dialog.CaptionDialog +import com.keylesspalace.tusky.components.compose.dialog.makeFocusDialog +import com.keylesspalace.tusky.components.compose.dialog.showAddPollDialog +import com.keylesspalace.tusky.components.compose.view.ComposeOptionsListener +import com.keylesspalace.tusky.components.compose.view.ComposeScheduleView +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository +import com.keylesspalace.tusky.databinding.ActivityComposeBinding +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.db.entity.DraftAttachment +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.NewPoll +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.settings.AppTheme +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.settings.PrefKeys.APP_THEME +import com.keylesspalace.tusky.util.MentionSpan +import com.keylesspalace.tusky.util.PickMediaFiles +import com.keylesspalace.tusky.util.defaultFinders +import com.keylesspalace.tusky.util.getInitialLanguages +import com.keylesspalace.tusky.util.getLocaleList +import com.keylesspalace.tusky.util.getMediaSize +import com.keylesspalace.tusky.util.getParcelableArrayListExtraCompat +import com.keylesspalace.tusky.util.getParcelableCompat +import com.keylesspalace.tusky.util.getParcelableExtraCompat +import com.keylesspalace.tusky.util.getSerializableCompat +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.highlightSpans +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.modernLanguageCode +import com.keylesspalace.tusky.util.setDrawableTint +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import dagger.hilt.android.AndroidEntryPoint +import dagger.hilt.android.migration.OptionalInject +import java.io.File +import java.io.IOException +import java.text.DecimalFormat +import java.util.Locale +import kotlin.math.max +import kotlin.math.min +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize + +@OptionalInject +@AndroidEntryPoint +class ComposeActivity : + BaseActivity(), + ComposeOptionsListener, + ComposeAutoCompleteAdapter.AutocompletionProvider, + OnEmojiSelectedListener, + OnReceiveContentListener, + ComposeScheduleView.OnTimeSetListener, + CaptionDialog.Listener { + + private lateinit var composeOptionsBehavior: BottomSheetBehavior<*> + private lateinit var addMediaBehavior: BottomSheetBehavior<*> + private lateinit var emojiBehavior: BottomSheetBehavior<*> + private lateinit var scheduleBehavior: BottomSheetBehavior<*> + + /** The account that is being used to compose the status */ + private lateinit var activeAccount: AccountEntity + + private var photoUploadUri: Uri? = null + + @VisibleForTesting + var highlightFinders = defaultFinders + + @VisibleForTesting + var maximumTootCharacters = InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT + var charactersReservedPerUrl = InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL + + private val viewModel: ComposeViewModel by viewModels() + + private val binding by viewBinding(ActivityComposeBinding::inflate) + + private var maxUploadMediaNumber = InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS + + private val takePictureLauncher = + registerForActivityResult(ActivityResultContracts.TakePicture()) { success -> + if (success) { + pickMedia(photoUploadUri!!) + } + } + private val pickMediaFilePermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + pickMediaFileLauncher.launch(true) + } else { + Snackbar.make( + binding.activityCompose, + R.string.error_media_upload_permission, + Snackbar.LENGTH_SHORT + ).apply { + setAction(R.string.action_retry) { onMediaPick() } + // necessary so snackbar is shown over everything + view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) + show() + } + } + } + private val pickMediaFileLauncher = registerForActivityResult(PickMediaFiles()) { uris -> + if (viewModel.media.value.size + uris.size > maxUploadMediaNumber) { + Toast.makeText( + this, + resources.getQuantityString( + R.plurals.error_upload_max_media_reached, + maxUploadMediaNumber, + maxUploadMediaNumber + ), + Toast.LENGTH_SHORT + ).show() + } else { + uris.forEach { uri -> + pickMedia(uri) + } + } + } + + // Contract kicked off by editImageInQueue; expects viewModel.cropImageItemOld set + private val cropImage = registerForActivityResult(CropImageContract()) { result -> + val uriNew = result.uriContent + if (result.isSuccessful && uriNew != null) { + viewModel.cropImageItemOld?.let { itemOld -> + val size = getMediaSize(contentResolver, uriNew) + + lifecycleScope.launch { + viewModel.addMediaToQueue( + itemOld.type, + uriNew, + size, + itemOld.description, + // Intentionally reset focus when cropping + null, + itemOld + ) + } + } + } else if (result == CropImage.CancelledResult) { + Log.w("ComposeActivity", "Edit image cancelled by user") + } else { + Log.w("ComposeActivity", "Edit image failed: " + result.error) + displayTransientMessage(R.string.error_image_edit_failed) + } + viewModel.cropImageItemOld = null + } + + private val onBackPressedCallback = object : OnBackPressedCallback(false) { + override fun handleOnBackPressed() { + if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_EXPANDED || + addMediaBehavior.state == BottomSheetBehavior.STATE_EXPANDED || + emojiBehavior.state == BottomSheetBehavior.STATE_EXPANDED || + scheduleBehavior.state == BottomSheetBehavior.STATE_EXPANDED + ) { + composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + return + } + + handleCloseButton() + } + } + + public override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + activeAccount = accountManager.activeAccount ?: return + + val theme = preferences.getString(APP_THEME, AppTheme.DEFAULT.value) + if (theme == "black") { + setTheme(R.style.TuskyDialogActivityBlackTheme) + } + setContentView(binding.root) + + setupActionBar() + + setupAvatar(activeAccount) + val mediaAdapter = MediaPreviewAdapter( + this, + onAddCaption = { item -> + CaptionDialog.newInstance( + item.localId, + item.description, + item.uri + ).show(supportFragmentManager, "caption_dialog") + }, + onAddFocus = { item -> + makeFocusDialog(item.focus, item.uri) { newFocus -> + viewModel.updateFocus(item.localId, newFocus) + } + // TODO this is inconsistent to CaptionDialog (device rotation)? + }, + onEditImage = this::editImageInQueue, + onRemove = this::removeMediaFromQueue + ) + binding.composeMediaPreviewBar.layoutManager = + LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) + binding.composeMediaPreviewBar.adapter = mediaAdapter + binding.composeMediaPreviewBar.itemAnimator = null + + /* If the composer is started up as a reply to another post, override the "starting" state + * based on what the intent from the reply request passes. */ + val composeOptions: ComposeOptions? = intent.getParcelableExtraCompat(COMPOSE_OPTIONS_EXTRA) + viewModel.setup(composeOptions) + + setupButtons() + subscribeToUpdates(mediaAdapter) + + if (accountManager.shouldDisplaySelfUsername()) { + binding.composeUsernameView.text = getString( + R.string.compose_active_account_description, + activeAccount.fullName + ) + binding.composeUsernameView.show() + } else { + binding.composeUsernameView.hide() + } + + setupReplyViews(composeOptions?.replyingStatusAuthor, composeOptions?.replyingStatusContent) + val statusContent = composeOptions?.content + if (!statusContent.isNullOrEmpty()) { + binding.composeEditField.setText(statusContent) + } + + if (!composeOptions?.scheduledAt.isNullOrEmpty()) { + binding.composeScheduleView.setDateTime(composeOptions?.scheduledAt) + } + + setupLanguageSpinner(getInitialLanguages(composeOptions?.language, activeAccount)) + setupComposeField(preferences, viewModel.startingText) + setupContentWarningField(composeOptions?.contentWarning) + setupPollView() + applyShareIntent(intent, savedInstanceState) + + /* Finally, overwrite state with data from saved instance state. */ + savedInstanceState?.let { + photoUploadUri = it.getParcelableCompat(PHOTO_UPLOAD_URI_KEY) + + setStatusVisibility(it.getSerializableCompat(VISIBILITY_KEY)!!) + + it.getBoolean(CONTENT_WARNING_VISIBLE_KEY).apply { + viewModel.contentWarningChanged(this) + } + + it.getString(SCHEDULED_TIME_KEY)?.let { time -> + viewModel.updateScheduledAt(time) + } + } + + binding.composeEditField.post { + binding.composeEditField.requestFocus() + } + } + + private fun applyShareIntent(intent: Intent, savedInstanceState: Bundle?) { + if (savedInstanceState == null) { + /* Get incoming images being sent through a share action from another app. Only do this + * when savedInstanceState is null, otherwise both the images from the intent and the + * instance state will be re-queued. */ + intent.type?.also { type -> + if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) { + when (intent.action) { + Intent.ACTION_SEND -> { + intent.getParcelableExtraCompat<Uri>(Intent.EXTRA_STREAM)?.let { uri -> + pickMedia(uri) + } + } + Intent.ACTION_SEND_MULTIPLE -> { + intent.getParcelableArrayListExtraCompat<Uri>(Intent.EXTRA_STREAM) + ?.forEach { uri -> + pickMedia(uri) + } + } + } + } + + val subject = intent.getStringExtra(Intent.EXTRA_SUBJECT) + val text = intent.getStringExtra(Intent.EXTRA_TEXT).orEmpty() + val shareBody = if (!subject.isNullOrBlank() && subject !in text) { + subject + '\n' + text + } else { + text + } + + if (shareBody.isNotBlank()) { + val start = binding.composeEditField.selectionStart.coerceAtLeast(0) + val end = binding.composeEditField.selectionEnd.coerceAtLeast(0) + val left = min(start, end) + val right = max(start, end) + binding.composeEditField.text.replace( + left, + right, + shareBody, + 0, + shareBody.length + ) + // move edittext cursor to first when shareBody parsed + binding.composeEditField.text.insert(0, "\n") + binding.composeEditField.setSelection(0) + } + } + } + } + + private fun setupReplyViews(replyingStatusAuthor: String?, replyingStatusContent: String?) { + if (replyingStatusAuthor != null) { + binding.composeReplyView.show() + binding.composeReplyView.text = getString(R.string.replying_to, replyingStatusAuthor) + val arrowDownIcon = IconicsDrawable( + this, + GoogleMaterial.Icon.gmd_arrow_drop_down + ).apply { + sizeDp = 12 + } + + setDrawableTint(this, arrowDownIcon, android.R.attr.textColorTertiary) + binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds( + null, + null, + arrowDownIcon, + null + ) + + binding.composeReplyView.setOnClickListener { + TransitionManager.beginDelayedTransition( + binding.composeReplyContentView.parent as ViewGroup + ) + + if (binding.composeReplyContentView.isVisible) { + binding.composeReplyContentView.hide() + binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds( + null, + null, + arrowDownIcon, + null + ) + } else { + binding.composeReplyContentView.show() + val arrowUpIcon = IconicsDrawable( + this, + GoogleMaterial.Icon.gmd_arrow_drop_up + ).apply { sizeDp = 12 } + + setDrawableTint(this, arrowUpIcon, android.R.attr.textColorTertiary) + binding.composeReplyView.setCompoundDrawablesRelativeWithIntrinsicBounds( + null, + null, + arrowUpIcon, + null + ) + } + } + } + replyingStatusContent?.let { binding.composeReplyContentView.text = it } + } + + private fun setupContentWarningField(startingContentWarning: String?) { + if (startingContentWarning != null) { + binding.composeContentWarningField.setText(startingContentWarning) + } + binding.composeContentWarningField.doOnTextChanged { newContentWarning, _, _, _ -> + updateVisibleCharactersLeft() + viewModel.updateContentWarning(newContentWarning?.toString()) + } + } + + private fun setupComposeField(preferences: SharedPreferences, startingText: String?) { + binding.composeEditField.setOnReceiveContentListener(this) + + binding.composeEditField.setOnKeyListener { _, keyCode, event -> + this.onKeyDown( + keyCode, + event + ) + } + + binding.composeEditField.setAdapter( + ComposeAutoCompleteAdapter( + this, + preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), + preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true) + ) + ) + binding.composeEditField.setTokenizer(ComposeTokenizer()) + + binding.composeEditField.setText(startingText) + binding.composeEditField.setSelection(binding.composeEditField.length()) + + val mentionColour = binding.composeEditField.linkTextColors.defaultColor + binding.composeEditField.text.highlightSpans(mentionColour, highlightFinders) + binding.composeEditField.doAfterTextChanged { editable -> + editable!!.highlightSpans(mentionColour, highlightFinders) + updateVisibleCharactersLeft() + viewModel.updateContent(editable.toString()) + } + + // work around Android platform bug -> https://issuetracker.google.com/issues/67102093 + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O || + Build.VERSION.SDK_INT == Build.VERSION_CODES.O_MR1 + ) { + binding.composeEditField.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + } + } + + private fun subscribeToUpdates(mediaAdapter: MediaPreviewAdapter) { + lifecycleScope.launch { + viewModel.instanceInfo.collect { instanceData -> + maximumTootCharacters = instanceData.maxChars + charactersReservedPerUrl = instanceData.charactersReservedPerUrl + maxUploadMediaNumber = instanceData.maxMediaAttachments + updateVisibleCharactersLeft() + } + } + + lifecycleScope.launch { + viewModel.emoji.collect(::setEmojiList) + } + + lifecycleScope.launch { + viewModel.showContentWarning.combine( + viewModel.markMediaAsSensitive + ) { showContentWarning, markSensitive -> + updateSensitiveMediaToggle(markSensitive, showContentWarning) + showContentWarning(showContentWarning) + }.collect() + } + + lifecycleScope.launch { + viewModel.statusVisibility.collect(::setStatusVisibility) + } + + lifecycleScope.launch { + viewModel.media.collect { media -> + mediaAdapter.submitList(media) + + binding.composeMediaPreviewBar.visible(media.isNotEmpty()) + updateSensitiveMediaToggle( + viewModel.markMediaAsSensitive.value, + viewModel.showContentWarning.value + ) + } + } + + lifecycleScope.launch { + viewModel.poll.collect { poll -> + binding.pollPreview.visible(poll != null) + poll?.let(binding.pollPreview::setPoll) + } + } + + lifecycleScope.launch { + viewModel.scheduledAt.collect { scheduledAt -> + if (scheduledAt == null) { + binding.composeScheduleView.resetSchedule() + } else { + binding.composeScheduleView.setDateTime(scheduledAt) + } + updateScheduleButton() + } + } + + lifecycleScope.launch { + viewModel.media.combine(viewModel.poll) { media, poll -> + val active = poll == null && + media.size < maxUploadMediaNumber && + (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE) + enableButton(binding.composeAddMediaButton, active, active) + enablePollButton(media.isEmpty()) + }.collect() + } + + lifecycleScope.launch { + viewModel.uploadError.collect { throwable -> + if (throwable is UploadServerError) { + displayTransientMessage(throwable.errorMessage) + } else { + displayTransientMessage( + getString( + R.string.error_media_upload_sending_fmt, + throwable.message + ) + ) + } + } + } + + lifecycleScope.launch { + viewModel.closeConfirmation.collect { + updateOnBackPressedCallbackState() + } + } + } + + private fun setupButtons() { + binding.composeOptionsBottomSheet.listener = this + + composeOptionsBehavior = BottomSheetBehavior.from(binding.composeOptionsBottomSheet) + addMediaBehavior = BottomSheetBehavior.from(binding.addMediaBottomSheet) + scheduleBehavior = BottomSheetBehavior.from(binding.composeScheduleView) + emojiBehavior = BottomSheetBehavior.from(binding.emojiView) + + val bottomSheetCallback = object : BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + updateOnBackPressedCallbackState() + } + override fun onSlide(bottomSheet: View, slideOffset: Float) { } + } + composeOptionsBehavior.addBottomSheetCallback(bottomSheetCallback) + addMediaBehavior.addBottomSheetCallback(bottomSheetCallback) + scheduleBehavior.addBottomSheetCallback(bottomSheetCallback) + emojiBehavior.addBottomSheetCallback(bottomSheetCallback) + + enableButton(binding.composeEmojiButton, clickable = false, colorActive = false) + + // Setup the interface buttons. + binding.composeTootButton.setOnClickListener { onSendClicked() } + binding.composeAddMediaButton.setOnClickListener { openPickDialog() } + binding.composeToggleVisibilityButton.setOnClickListener { showComposeOptions() } + binding.composeContentWarningButton.setOnClickListener { onContentWarningChanged() } + binding.composeEmojiButton.setOnClickListener { showEmojis() } + binding.composeHideMediaButton.setOnClickListener { toggleHideMedia() } + binding.composeScheduleButton.setOnClickListener { onScheduleClick() } + binding.composeScheduleView.setResetOnClickListener { resetSchedule() } + binding.composeScheduleView.setListener(this) + binding.atButton.setOnClickListener { atButtonClicked() } + binding.hashButton.setOnClickListener { hashButtonClicked() } + binding.descriptionMissingWarningButton.setOnClickListener { + displayTransientMessage(R.string.hint_media_description_missing) + } + + val textColor = MaterialColors.getColor(binding.root, android.R.attr.textColorTertiary) + + val cameraIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_camera_alt).apply { + colorInt = textColor + sizeDp = 18 + } + binding.actionPhotoTake.setCompoundDrawablesRelativeWithIntrinsicBounds( + cameraIcon, + null, + null, + null + ) + + val imageIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_image).apply { + colorInt = textColor + sizeDp = 18 + } + binding.actionPhotoPick.setCompoundDrawablesRelativeWithIntrinsicBounds( + imageIcon, + null, + null, + null + ) + + val pollIcon = IconicsDrawable(this, GoogleMaterial.Icon.gmd_poll).apply { + colorInt = textColor + sizeDp = 18 + } + binding.addPollTextActionTextView.setCompoundDrawablesRelativeWithIntrinsicBounds( + pollIcon, + null, + null, + null + ) + + binding.actionPhotoTake.visible( + Intent(MediaStore.ACTION_IMAGE_CAPTURE).resolveActivity(packageManager) != null + ) + + binding.actionPhotoTake.setOnClickListener { initiateCameraApp() } + binding.actionPhotoPick.setOnClickListener { onMediaPick() } + binding.addPollTextActionTextView.setOnClickListener { openPollDialog() } + + onBackPressedDispatcher.addCallback(this, onBackPressedCallback) + } + + private fun setupLanguageSpinner(initialLanguages: List<String>) { + binding.composePostLanguageButton.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>, + view: View?, + position: Int, + id: Long + ) { + viewModel.postLanguage = (parent.adapter.getItem(position) as Locale).modernLanguageCode + } + + override fun onNothingSelected(parent: AdapterView<*>) { + parent.setSelection(0) + } + } + binding.composePostLanguageButton.apply { + adapter = LocaleAdapter(context, android.R.layout.simple_spinner_dropdown_item, getLocaleList(initialLanguages)) + setSelection(0) + } + } + + private fun setupActionBar() { + setSupportActionBar(binding.toolbar) + supportActionBar?.run { + title = null + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + setHomeAsUpIndicator(R.drawable.ic_close_24dp) + } + } + + private fun setupAvatar(activeAccount: AccountEntity) { + val actionBarSizeAttr = intArrayOf(androidx.appcompat.R.attr.actionBarSize) + val avatarSize = obtainStyledAttributes(null, actionBarSizeAttr).use { a -> + a.getDimensionPixelSize(0, 1) + } + + val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) + loadAvatar( + activeAccount.profilePictureUrl, + binding.composeAvatar, + avatarSize / 8, + animateAvatars + ) + binding.composeAvatar.contentDescription = getString( + R.string.compose_active_account_description, + activeAccount.fullName + ) + } + + private fun updateOnBackPressedCallbackState() { + val confirmation = viewModel.closeConfirmation.value + onBackPressedCallback.isEnabled = confirmation != ConfirmationKind.NONE || + composeOptionsBehavior.state != BottomSheetBehavior.STATE_HIDDEN || + addMediaBehavior.state != BottomSheetBehavior.STATE_HIDDEN || + emojiBehavior.state != BottomSheetBehavior.STATE_HIDDEN || + scheduleBehavior.state != BottomSheetBehavior.STATE_HIDDEN + } + + private fun replaceTextAtCaret(text: CharSequence) { + // If you select "backward" in an editable, you get SelectionStart > SelectionEnd + val start = binding.composeEditField.selectionStart.coerceAtMost( + binding.composeEditField.selectionEnd + ) + val end = binding.composeEditField.selectionStart.coerceAtLeast( + binding.composeEditField.selectionEnd + ) + val textToInsert = if (start > 0 && !binding.composeEditField.text[start - 1].isWhitespace()) { + " $text" + } else { + text + } + binding.composeEditField.text.replace(start, end, textToInsert) + + // Set the cursor after the inserted text + binding.composeEditField.setSelection(start + text.length) + } + + fun prependSelectedWordsWith(text: CharSequence) { + // If you select "backward" in an editable, you get SelectionStart > SelectionEnd + val start = binding.composeEditField.selectionStart.coerceAtMost( + binding.composeEditField.selectionEnd + ) + val end = binding.composeEditField.selectionStart.coerceAtLeast( + binding.composeEditField.selectionEnd + ) + val editorText = binding.composeEditField.text + + if (start == end) { + // No selection, just insert text at caret + editorText.insert(start, text) + // Set the cursor after the inserted text + binding.composeEditField.setSelection(start + text.length) + } else { + var wasWord: Boolean + var isWord = end < editorText.length && !Character.isWhitespace(editorText[end]) + var newEnd = end + + // Iterate the selection backward so we don't have to juggle indices on insertion + var index = end - 1 + while (index >= start - 1 && index >= 0) { + wasWord = isWord + isWord = !Character.isWhitespace(editorText[index]) + if (wasWord && !isWord) { + // We've reached the beginning of a word, perform insert + editorText.insert(index + 1, text) + newEnd += text.length + } + --index + } + + if (start == 0 && isWord) { + // Special case when the selection includes the start of the text + editorText.insert(0, text) + newEnd += text.length + } + + // Keep the same text (including insertions) selected + binding.composeEditField.setSelection(start, newEnd) + } + } + + private fun atButtonClicked() { + prependSelectedWordsWith("@") + } + + private fun hashButtonClicked() { + prependSelectedWordsWith("#") + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putParcelable(PHOTO_UPLOAD_URI_KEY, photoUploadUri) + outState.putSerializable(VISIBILITY_KEY, viewModel.statusVisibility.value) + outState.putBoolean(CONTENT_WARNING_VISIBLE_KEY, viewModel.showContentWarning.value) + outState.putString(SCHEDULED_TIME_KEY, viewModel.scheduledAt.value) + super.onSaveInstanceState(outState) + } + + private fun displayTransientMessage(message: String) { + val bar = Snackbar.make(binding.activityCompose, message, Snackbar.LENGTH_LONG) + // necessary so snackbar is shown over everything + bar.view.elevation = resources.getDimension(R.dimen.compose_activity_snackbar_elevation) + bar.setAnchorView(R.id.composeBottomBar) + bar.show() + } + private fun displayTransientMessage(@StringRes stringId: Int) { + displayTransientMessage(getString(stringId)) + } + + private fun toggleHideMedia() { + this.viewModel.toggleMarkSensitive() + } + + private fun updateSensitiveMediaToggle( + markMediaSensitive: Boolean, + contentWarningShown: Boolean + ) { + if (viewModel.media.value.isEmpty()) { + binding.composeHideMediaButton.hide() + binding.descriptionMissingWarningButton.hide() + } else { + binding.composeHideMediaButton.show() + @AttrRes val color = if (contentWarningShown) { + binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) + binding.composeHideMediaButton.isClickable = false + materialR.attr.colorPrimary + } else { + binding.composeHideMediaButton.isClickable = true + if (markMediaSensitive) { + binding.composeHideMediaButton.setImageResource(R.drawable.ic_hide_media_24dp) + materialR.attr.colorPrimary + } else { + binding.composeHideMediaButton.setImageResource(R.drawable.ic_eye_24dp) + android.R.attr.textColorTertiary + } + } + binding.composeHideMediaButton.drawable.setTint( + MaterialColors.getColor( + binding.composeHideMediaButton, + color + ) + ) + + var oneMediaWithoutDescription = false + for (media in viewModel.media.value) { + if (media.description.isNullOrEmpty()) { + oneMediaWithoutDescription = true + break + } + } + binding.descriptionMissingWarningButton.visibility = if (oneMediaWithoutDescription) View.VISIBLE else View.GONE + } + } + + private fun updateScheduleButton() { + if (viewModel.editing) { + // Can't reschedule a published status + enableButton(binding.composeScheduleButton, clickable = false, colorActive = false) + } else { + @ColorInt val color = + MaterialColors.getColor( + binding.composeScheduleButton, + if (binding.composeScheduleView.time == null) { + android.R.attr.textColorTertiary + } else { + materialR.attr.colorPrimary + } + ) + binding.composeScheduleButton.drawable.setTint(color) + } + } + + private fun enableButtons(enable: Boolean, editing: Boolean) { + binding.composeAddMediaButton.isClickable = enable + binding.composeToggleVisibilityButton.isClickable = enable && !editing + binding.composeEmojiButton.isClickable = enable + binding.composeHideMediaButton.isClickable = enable + binding.composeScheduleButton.isClickable = enable && !editing + binding.composeTootButton.isEnabled = enable + } + + private fun setStatusVisibility(visibility: Status.Visibility) { + binding.composeOptionsBottomSheet.setStatusVisibility(visibility) + binding.composeTootButton.setStatusVisibility(visibility) + + val iconRes = when (visibility) { + Status.Visibility.PUBLIC -> R.drawable.ic_public_24dp + Status.Visibility.PRIVATE -> R.drawable.ic_lock_outline_24dp + Status.Visibility.DIRECT -> R.drawable.ic_email_24dp + Status.Visibility.UNLISTED -> R.drawable.ic_lock_open_24dp + else -> R.drawable.ic_lock_open_24dp + } + binding.composeToggleVisibilityButton.setImageResource(iconRes) + if (viewModel.editing) { + // Can't update visibility on published status + enableButton( + binding.composeToggleVisibilityButton, + clickable = false, + colorActive = false + ) + } + } + + private fun showComposeOptions() { + if (composeOptionsBehavior.state == BottomSheetBehavior.STATE_HIDDEN || composeOptionsBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + composeOptionsBehavior.state = BottomSheetBehavior.STATE_EXPANDED + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) + } else { + composeOptionsBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) + } + } + + private fun onScheduleClick() { + if (viewModel.scheduledAt.value == null) { + binding.composeScheduleView.openPickDateDialog() + } else { + showScheduleView() + } + } + + private fun showScheduleView() { + if (scheduleBehavior.state == BottomSheetBehavior.STATE_HIDDEN || scheduleBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + scheduleBehavior.state = BottomSheetBehavior.STATE_EXPANDED + composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) + } else { + scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) + } + } + + private fun showEmojis() { + binding.emojiView.adapter?.let { + if (it.itemCount == 0) { + val errorMessage = + getString( + R.string.error_no_custom_emojis, + accountManager.activeAccount!!.domain + ) + displayTransientMessage(errorMessage) + } else { + if (emojiBehavior.state == BottomSheetBehavior.STATE_HIDDEN || emojiBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + emojiBehavior.state = BottomSheetBehavior.STATE_EXPANDED + composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + addMediaBehavior.state = BottomSheetBehavior.STATE_HIDDEN + scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) + } else { + emojiBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) + } + } + } + } + + private fun openPickDialog() { + if (addMediaBehavior.state == BottomSheetBehavior.STATE_HIDDEN || addMediaBehavior.state == BottomSheetBehavior.STATE_COLLAPSED) { + addMediaBehavior.state = BottomSheetBehavior.STATE_EXPANDED + composeOptionsBehavior.state = BottomSheetBehavior.STATE_HIDDEN + emojiBehavior.state = BottomSheetBehavior.STATE_HIDDEN + scheduleBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) + } else { + addMediaBehavior.setState(BottomSheetBehavior.STATE_HIDDEN) + } + } + + private fun onMediaPick() { + addMediaBehavior.addBottomSheetCallback( + object : BottomSheetCallback() { + override fun onStateChanged(bottomSheet: View, newState: Int) { + // Wait until bottom sheet is not collapsed and show next screen after + if (newState == BottomSheetBehavior.STATE_COLLAPSED) { + addMediaBehavior.removeBottomSheetCallback(this) + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + pickMediaFilePermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) + } else { + pickMediaFileLauncher.launch(true) + } + } + } + + override fun onSlide(bottomSheet: View, slideOffset: Float) {} + } + ) + addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + } + + private fun openPollDialog() = lifecycleScope.launch { + addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + val instanceParams = viewModel.instanceInfo.first() + showAddPollDialog( + context = this@ComposeActivity, + poll = viewModel.poll.value, + maxOptionCount = instanceParams.pollMaxOptions, + maxOptionLength = instanceParams.pollMaxLength, + minDuration = instanceParams.pollMinDuration, + maxDuration = instanceParams.pollMaxDuration, + onUpdatePoll = viewModel::updatePoll + ) + } + + private fun setupPollView() { + val margin = resources.getDimensionPixelSize(R.dimen.compose_media_preview_margin) + val marginBottom = resources.getDimensionPixelSize( + R.dimen.compose_media_preview_margin_bottom + ) + + val layoutParams = LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + layoutParams.setMargins(margin, margin, margin, marginBottom) + binding.pollPreview.layoutParams = layoutParams + + binding.pollPreview.setOnClickListener { + val popup = PopupMenu(this, binding.pollPreview) + val editId = 1 + val removeId = 2 + popup.menu.add(0, editId, 0, R.string.edit_poll) + popup.menu.add(0, removeId, 0, R.string.action_remove) + popup.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + editId -> openPollDialog() + removeId -> removePoll() + } + true + } + popup.show() + } + } + + private fun removePoll() { + viewModel.updatePoll(null) + binding.pollPreview.hide() + } + + override fun onVisibilityChanged(visibility: Status.Visibility) { + composeOptionsBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + viewModel.changeStatusVisibility(visibility) + } + + @VisibleForTesting + fun calculateTextLength(): Int { + return statusLength( + binding.composeEditField.text, + binding.composeContentWarningField.text, + charactersReservedPerUrl + ) + } + + @VisibleForTesting + val selectedLanguage: String? + get() = viewModel.postLanguage + + private fun updateVisibleCharactersLeft() { + val remainingLength = maximumTootCharacters - calculateTextLength() + binding.composeCharactersLeftView.text = String.format(Locale.getDefault(), "%d", remainingLength) + + val textColor = if (remainingLength < 0) { + getColor(R.color.warning_color) + } else { + MaterialColors.getColor( + binding.composeCharactersLeftView, + android.R.attr.textColorTertiary + ) + } + binding.composeCharactersLeftView.setTextColor(textColor) + } + + private fun onContentWarningChanged() { + val showWarning = binding.composeContentWarningBar.isGone + viewModel.contentWarningChanged(showWarning) + updateVisibleCharactersLeft() + } + + private fun verifyScheduledTime(): Boolean { + return binding.composeScheduleView.verifyScheduledTime( + binding.composeScheduleView.getDateTime(viewModel.scheduledAt.value) + ) + } + + private fun onSendClicked() { + if (verifyScheduledTime()) { + sendStatus() + } else { + showScheduleView() + } + } + + /** This is for the fancy keyboards which can insert images and stuff, and drag&drop etc */ + override fun onReceiveContent(view: View, contentInfo: ContentInfoCompat): ContentInfoCompat? { + if (contentInfo.clip.description.hasMimeType("image/*")) { + val split = contentInfo.partition { item: ClipData.Item -> item.uri != null } + split.first?.let { content -> + for (i in 0 until content.clip.itemCount) { + pickMedia( + content.clip.getItemAt(i).uri, + contentInfo.clip.description.label as String? + ) + } + } + return split.second + } + return contentInfo + } + + private fun sendStatus() { + enableButtons(false, viewModel.editing) + val contentText = binding.composeEditField.text.toString() + var spoilerText = "" + if (viewModel.showContentWarning.value) { + spoilerText = binding.composeContentWarningField.text.toString() + } + val characterCount = calculateTextLength() + if ((characterCount <= 0 || contentText.isBlank()) && viewModel.media.value.isEmpty()) { + binding.composeEditField.error = getString(R.string.error_empty) + enableButtons(true, viewModel.editing) + } else if (characterCount <= maximumTootCharacters) { + lifecycleScope.launch { + viewModel.sendStatus(contentText, spoilerText, activeAccount.id) + deleteDraftAndFinish() + } + } else { + binding.composeEditField.error = getString(R.string.error_compose_character_limit) + enableButtons(true, viewModel.editing) + } + } + + private fun initiateCameraApp() { + addMediaBehavior.state = BottomSheetBehavior.STATE_COLLAPSED + + val photoFile: File = try { + createNewImageFile(this) + } catch (ex: IOException) { + displayTransientMessage(R.string.error_media_upload_opening) + return + } + + // Continue only if the File was successfully created + photoUploadUri = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID + ".fileprovider", + photoFile + ).also { uri -> takePictureLauncher.launch(uri) } + } + + private fun enableButton(button: ImageButton, clickable: Boolean, colorActive: Boolean) { + button.isEnabled = clickable + setDrawableTint( + this, + button.drawable, + if (colorActive) { + android.R.attr.textColorTertiary + } else { + R.attr.textColorDisabled + } + ) + } + + private fun enablePollButton(enable: Boolean) { + binding.addPollTextActionTextView.isEnabled = enable + val textColor = MaterialColors.getColor( + binding.addPollTextActionTextView, + if (enable) { + android.R.attr.textColorTertiary + } else { + R.attr.textColorDisabled + } + ) + binding.addPollTextActionTextView.setTextColor(textColor) + binding.addPollTextActionTextView.compoundDrawablesRelative[0].setTint(textColor) + } + + private fun editImageInQueue(item: QueuedMedia) { + // If input image is lossless, output image should be lossless. + // Currently the only supported lossless format is png. + val mimeType: String? = contentResolver.getType(item.uri) + val isPng: Boolean = mimeType != null && mimeType.endsWith("/png") + val tempFile = createNewImageFile(this, if (isPng) ".png" else ".jpg") + + // "Authority" must be the same as the android:authorities string in AndroidManifest.xml + val uriNew = FileProvider.getUriForFile( + this, + BuildConfig.APPLICATION_ID + ".fileprovider", + tempFile + ) + + viewModel.cropImageItemOld = item + + cropImage.launch( + options(uri = item.uri) { + setOutputUri(uriNew) + setOutputCompressFormat( + if (isPng) Bitmap.CompressFormat.PNG else Bitmap.CompressFormat.JPEG + ) + } + ) + } + + private fun removeMediaFromQueue(item: QueuedMedia) { + viewModel.removeMediaFromQueue(item) + } + + private fun sanitizePickMediaDescription(description: String?): String? { + if (description == null) { + return null + } + + // The Gboard android keyboard attaches this text whenever the user + // pastes something from the keyboard's suggestion bar. + // Due to different end user locales, the exact text may vary, but at + // least in version 13.4.08, all of the translations contained the + // string "Gboard". + if ("Gboard" in description) { + return null + } + + return description + } + + private fun pickMedia(uri: Uri, description: String? = null) { + val sanitizedDescription = sanitizePickMediaDescription(description) + + lifecycleScope.launch { + viewModel.pickMedia(uri, sanitizedDescription).onFailure { throwable -> + val errorString = when (throwable) { + is FileSizeException -> { + val decimalFormat = DecimalFormat("0.##") + val allowedSizeInMb = throwable.allowedSizeInBytes.toDouble() / (1024 * 1024) + val formattedSize = decimalFormat.format(allowedSizeInMb) + getString(R.string.error_multimedia_size_limit, formattedSize) + } + is VideoOrImageException -> getString( + R.string.error_media_upload_image_or_video + ) + else -> getString(R.string.error_media_upload_opening) + } + displayTransientMessage(errorString) + } + } + } + + private fun showContentWarning(show: Boolean) { + TransitionManager.beginDelayedTransition( + binding.composeContentWarningBar.parent as ViewGroup + ) + @AttrRes val color = if (show) { + binding.composeContentWarningBar.show() + binding.composeContentWarningField.setSelection( + binding.composeContentWarningField.text.length + ) + binding.composeContentWarningField.requestFocus() + materialR.attr.colorPrimary + } else { + binding.composeContentWarningBar.hide() + binding.composeEditField.requestFocus() + android.R.attr.textColorTertiary + } + binding.composeContentWarningButton.drawable.setTint( + MaterialColors.getColor( + binding.composeHideMediaButton, + color + ) + ) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (item.itemId == android.R.id.home) { + handleCloseButton() + return true + } + + return super.onOptionsItemSelected(item) + } + + override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean { + if (event.action == KeyEvent.ACTION_DOWN) { + if (event.isCtrlPressed) { + if (keyCode == KeyEvent.KEYCODE_ENTER) { + // send toot by pressing CTRL + ENTER + this.onSendClicked() + return true + } + } + + if (keyCode == KeyEvent.KEYCODE_BACK) { + onBackPressedDispatcher.onBackPressed() + return true + } + } + return super.onKeyDown(keyCode, event) + } + + private fun handleCloseButton() { + val contentText = binding.composeEditField.text.toString() + val contentWarning = binding.composeContentWarningField.text.toString() + when (viewModel.closeConfirmation.value) { + ConfirmationKind.NONE -> { + viewModel.stopUploads() + finish() + } + ConfirmationKind.SAVE_OR_DISCARD -> + getSaveAsDraftOrDiscardDialog(contentText, contentWarning).show() + ConfirmationKind.UPDATE_OR_DISCARD -> + getUpdateDraftOrDiscardDialog(contentText, contentWarning).show() + ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_CHANGES -> + getContinueEditingOrDiscardDialog().show() + ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_DRAFT -> + getDeleteEmptyDraftOrContinueEditing().show() + } + } + + /** + * User is editing a new post, and can either save the changes as a draft or discard them. + */ + private fun getSaveAsDraftOrDiscardDialog( + contentText: String, + contentWarning: String + ): AlertDialog.Builder { + val warning = if (viewModel.media.value.isNotEmpty()) { + R.string.compose_save_draft_loses_media + } else { + R.string.compose_save_draft + } + + return AlertDialog.Builder(this) + .setMessage(warning) + .setPositiveButton(R.string.action_save) { _, _ -> + viewModel.stopUploads() + saveDraftAndFinish(contentText, contentWarning) + } + .setNegativeButton(R.string.action_delete) { _, _ -> + viewModel.stopUploads() + deleteDraftAndFinish() + } + } + + /** + * User is editing an existing draft, and can either update the draft with the new changes or + * discard them. + */ + private fun getUpdateDraftOrDiscardDialog( + contentText: String, + contentWarning: String + ): AlertDialog.Builder { + val warning = if (viewModel.media.value.isNotEmpty()) { + R.string.compose_save_draft_loses_media + } else { + R.string.compose_save_draft + } + + return AlertDialog.Builder(this) + .setMessage(warning) + .setPositiveButton(R.string.action_save) { _, _ -> + viewModel.stopUploads() + saveDraftAndFinish(contentText, contentWarning) + } + .setNegativeButton(R.string.action_discard) { _, _ -> + viewModel.stopUploads() + finish() + } + } + + /** + * User is editing a post (scheduled, or posted), and can either go back to editing, or + * discard the changes. + */ + private fun getContinueEditingOrDiscardDialog(): AlertDialog.Builder { + return AlertDialog.Builder(this) + .setMessage(R.string.compose_unsaved_changes) + .setPositiveButton(R.string.action_continue_edit) { _, _ -> + // Do nothing, dialog will dismiss, user can continue editing + } + .setNegativeButton(R.string.action_discard) { _, _ -> + viewModel.stopUploads() + finish() + } + } + + /** + * User is editing an existing draft and making it empty. + * The user can either delete the empty draft or go back to editing. + */ + private fun getDeleteEmptyDraftOrContinueEditing(): AlertDialog.Builder { + return AlertDialog.Builder(this) + .setMessage(R.string.compose_delete_draft) + .setPositiveButton(R.string.action_delete) { _, _ -> + viewModel.deleteDraft() + viewModel.stopUploads() + finish() + } + .setNegativeButton(R.string.action_continue_edit) { _, _ -> + // Do nothing, dialog will dismiss, user can continue editing + } + } + + private fun deleteDraftAndFinish() { + viewModel.deleteDraft() + finish() + } + + private fun saveDraftAndFinish(contentText: String, contentWarning: String) { + lifecycleScope.launch { + val dialog = if (viewModel.shouldShowSaveDraftDialog()) { + ProgressDialog.show( + this@ComposeActivity, + null, + getString(R.string.saving_draft), + true, + false + ) + } else { + null + } + viewModel.saveDraft(contentText, contentWarning) + dialog?.cancel() + finish() + } + } + + override fun search(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> { + return viewModel.searchAutocompleteSuggestions(token) + } + + override fun onEmojiSelected(shortcode: String) { + replaceTextAtCaret(":$shortcode: ") + } + + private fun setEmojiList(emojiList: List<Emoji>?) { + if (emojiList != null) { + val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + binding.emojiView.adapter = EmojiAdapter(emojiList, this@ComposeActivity, animateEmojis) + enableButton(binding.composeEmojiButton, true, emojiList.isNotEmpty()) + } + } + + data class QueuedMedia( + val localId: Int, + val uri: Uri, + val type: Type, + val mediaSize: Long, + val uploadPercent: Int = 0, + val id: String? = null, + val description: String? = null, + val focus: Attachment.Focus? = null, + val state: State + ) { + enum class Type { + IMAGE, + VIDEO, + AUDIO + } + enum class State { + UPLOADING, + UNPROCESSED, + PROCESSED, + PUBLISHED + } + } + + override fun onTimeSet(time: String?) { + viewModel.updateScheduledAt(time) + if (verifyScheduledTime()) { + scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } else { + showScheduleView() + } + } + + private fun resetSchedule() { + viewModel.updateScheduledAt(null) + scheduleBehavior.state = BottomSheetBehavior.STATE_HIDDEN + } + + override fun onUpdateDescription(localId: Int, description: String) { + viewModel.updateDescription(localId, description) + } + + /** + * Status' kind. This particularly affects how the status is handled if the user + * backs out of the edit. + */ + enum class ComposeKind { + /** Status is new */ + NEW, + + /** Editing a posted status */ + EDIT_POSTED, + + /** Editing a status started as an existing draft */ + EDIT_DRAFT, + + /** Editing an an existing scheduled status */ + EDIT_SCHEDULED + } + + @Parcelize + data class ComposeOptions( + // Let's keep fields var until all consumers are Kotlin + var scheduledTootId: String? = null, + var draftId: Int? = null, + var content: String? = null, + var mediaUrls: List<String>? = null, + var mediaDescriptions: List<String>? = null, + var mentionedUsernames: Set<String>? = null, + var inReplyToId: String? = null, + var replyVisibility: Status.Visibility? = null, + var visibility: Status.Visibility? = null, + var contentWarning: String? = null, + var replyingStatusAuthor: String? = null, + var replyingStatusContent: String? = null, + var mediaAttachments: List<Attachment>? = null, + var draftAttachments: List<DraftAttachment>? = null, + var scheduledAt: String? = null, + var sensitive: Boolean? = null, + var poll: NewPoll? = null, + var modifiedInitialState: Boolean? = null, + var language: String? = null, + var statusId: String? = null, + var kind: ComposeKind? = null + ) : Parcelable + + companion object { + private const val TAG = "ComposeActivity" // logging tag + + internal const val COMPOSE_OPTIONS_EXTRA = "COMPOSE_OPTIONS" + private const val PHOTO_UPLOAD_URI_KEY = "PHOTO_UPLOAD_URI" + private const val VISIBILITY_KEY = "VISIBILITY" + private const val SCHEDULED_TIME_KEY = "SCHEDULE" + private const val CONTENT_WARNING_VISIBLE_KEY = "CONTENT_WARNING_VISIBLE" + + /** + * @param options ComposeOptions to configure the ComposeActivity + * @return an Intent to start the ComposeActivity + */ + @JvmStatic + fun startIntent(context: Context, options: ComposeOptions): Intent { + return Intent(context, ComposeActivity::class.java).apply { + putExtra(COMPOSE_OPTIONS_EXTRA, options) + } + } + + fun canHandleMimeType(mimeType: String?): Boolean { + return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain") + } + + /** + * Calculate the effective status length. + * + * Some text is counted differently: + * + * In the status body: + * + * - URLs always count for [urlLength] characters irrespective of their actual length + * (https://docs.joinmastodon.org/user/posting/#links) + * - Mentions ("@user@some.instance") only count the "@user" part + * (https://docs.joinmastodon.org/user/posting/#mentions) + * - Hashtags are always treated as their actual length, including the "#" + * (https://docs.joinmastodon.org/user/posting/#hashtags) + * + * Content warning text is always treated as its full length, URLs and other entities + * are not treated differently. + * + * @param body status body text + * @param contentWarning optional content warning text + * @param urlLength the number of characters attributed to URLs + * @return the effective status length + */ + @JvmStatic + fun statusLength(body: Spanned, contentWarning: Spanned?, urlLength: Int): Int { + var length = body.toString().perceivedCharacterLength() - body.getSpans(0, body.length, URLSpan::class.java) + .fold(0) { acc, span -> + // Accumulate a count of characters to be *ignored* in the final length + acc + when (span) { + is MentionSpan -> { + // Ignore everything from the second "@" (if present) + span.url.length - ( + span.url.indexOf("@", 1).takeIf { it >= 0 } + ?: span.url.length + ) + } + else -> { + // Expected to be negative if the URL length < maxUrlLength + span.url.perceivedCharacterLength() - urlLength + } + } + } + + // Content warning text is treated as is, URLs or mentions there are not special + contentWarning?.let { length += it.toString().perceivedCharacterLength() } + return length + } + + // String.length would count emojis as multiple characters but Mastodon counts them as 1, so we need this workaround + private fun String.perceivedCharacterLength(): Int { + val breakIterator = BreakIterator.getCharacterInstance() + breakIterator.setText(this) + var count = 0 + while (breakIterator.next() != BreakIterator.DONE) { + count++ + } + return count + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.kt new file mode 100644 index 0000000..25d78d3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeAutoCompleteAdapter.kt @@ -0,0 +1,178 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.compose + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.Filter +import android.widget.Filterable +import androidx.annotation.WorkerThread +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemAutocompleteAccountBinding +import com.keylesspalace.tusky.databinding.ItemAutocompleteEmojiBinding +import com.keylesspalace.tusky.databinding.ItemAutocompleteHashtagBinding +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.visible + +class ComposeAutoCompleteAdapter( + private val autocompletionProvider: AutocompletionProvider, + private val animateAvatar: Boolean, + private val animateEmojis: Boolean, + private val showBotBadge: Boolean +) : BaseAdapter(), Filterable { + + private var resultList: List<AutocompleteResult> = emptyList() + + override fun getCount() = resultList.size + + override fun getItem(index: Int): AutocompleteResult { + return resultList[index] + } + + override fun getItemId(position: Int): Long { + return position.toLong() + } + + override fun getFilter(): Filter { + return object : Filter() { + + override fun convertResultToString(resultValue: Any): CharSequence { + return when (resultValue) { + is AutocompleteResult.AccountResult -> formatUsername(resultValue) + is AutocompleteResult.HashtagResult -> formatHashtag(resultValue) + is AutocompleteResult.EmojiResult -> formatEmoji(resultValue) + else -> "" + } + } + + @WorkerThread + override fun performFiltering(constraint: CharSequence?): FilterResults { + val filterResults = FilterResults() + if (constraint != null) { + val results = autocompletionProvider.search(constraint.toString()) + filterResults.values = results + filterResults.count = results.size + } + return filterResults + } + + @Suppress("UNCHECKED_CAST") + override fun publishResults(constraint: CharSequence?, results: FilterResults) { + if (results.count > 0) { + resultList = results.values as List<AutocompleteResult> + notifyDataSetChanged() + } else { + notifyDataSetInvalidated() + } + } + } + } + + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val itemViewType = getItemViewType(position) + val context = parent.context + + val view: View = convertView ?: run { + val layoutInflater = LayoutInflater.from(context) + val binding = when (itemViewType) { + ACCOUNT_VIEW_TYPE -> ItemAutocompleteAccountBinding.inflate(layoutInflater) + HASHTAG_VIEW_TYPE -> ItemAutocompleteHashtagBinding.inflate(layoutInflater) + EMOJI_VIEW_TYPE -> ItemAutocompleteEmojiBinding.inflate(layoutInflater) + else -> throw AssertionError("unknown view type") + } + binding.root.tag = binding + binding.root + } + + when (val binding = view.tag) { + is ItemAutocompleteAccountBinding -> { + val accountResult = getItem(position) as AutocompleteResult.AccountResult + val account = accountResult.account + binding.username.text = context.getString(R.string.post_username_format, account.username) + binding.displayName.text = account.name.emojify(account.emojis, binding.displayName, animateEmojis) + val avatarRadius = context.resources.getDimensionPixelSize( + R.dimen.avatar_radius_42dp + ) + loadAvatar( + account.avatar, + binding.avatar, + avatarRadius, + animateAvatar + ) + binding.avatarBadge.visible(showBotBadge && account.bot) + } + is ItemAutocompleteHashtagBinding -> { + val result = getItem(position) as AutocompleteResult.HashtagResult + binding.root.text = formatHashtag(result) + } + is ItemAutocompleteEmojiBinding -> { + val emojiResult = getItem(position) as AutocompleteResult.EmojiResult + val (shortcode, url) = emojiResult.emoji + binding.shortcode.text = context.getString(R.string.emoji_shortcode_format, shortcode) + Glide.with(binding.preview) + .load(url) + .into(binding.preview) + } + } + return view + } + + override fun getViewTypeCount() = 3 + + override fun getItemViewType(position: Int): Int { + return when (getItem(position)) { + is AutocompleteResult.AccountResult -> ACCOUNT_VIEW_TYPE + is AutocompleteResult.HashtagResult -> HASHTAG_VIEW_TYPE + is AutocompleteResult.EmojiResult -> EMOJI_VIEW_TYPE + } + } + + sealed interface AutocompleteResult { + class AccountResult(val account: TimelineAccount) : AutocompleteResult + + class HashtagResult(val hashtag: String) : AutocompleteResult + + class EmojiResult(val emoji: Emoji) : AutocompleteResult + } + + interface AutocompletionProvider { + fun search(token: String): List<AutocompleteResult> + } + + companion object { + private const val ACCOUNT_VIEW_TYPE = 0 + private const val HASHTAG_VIEW_TYPE = 1 + private const val EMOJI_VIEW_TYPE = 2 + + private fun formatUsername(result: AutocompleteResult.AccountResult): String { + return String.format("@%s", result.account.username) + } + + private fun formatHashtag(result: AutocompleteResult.HashtagResult): String { + return String.format("#%s", result.hashtag) + } + + private fun formatEmoji(result: AutocompleteResult.EmojiResult): String { + return String.format(":%s:", result.emoji.shortcode) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeTokenizer.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeTokenizer.kt new file mode 100644 index 0000000..99e68db --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeTokenizer.kt @@ -0,0 +1,107 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.compose + +import android.text.SpannableString +import android.text.Spanned +import android.text.TextUtils +import android.widget.MultiAutoCompleteTextView + +class ComposeTokenizer : MultiAutoCompleteTextView.Tokenizer { + + private fun isMentionOrHashtagAllowedCharacter(character: Char): Boolean { + return Character.isLetterOrDigit(character) || character == '_' || // simple usernames + character == '-' || // extended usernames + character == '.' // domain dot + } + + override fun findTokenStart(text: CharSequence, cursor: Int): Int { + if (cursor == 0) { + return cursor + } + var i = cursor + var character = text[i - 1] + + // go up to first illegal character or character we're looking for (@, # or :) + while (i > 0 && !(character == '@' || character == '#' || character == ':')) { + if (!isMentionOrHashtagAllowedCharacter(character)) { + return cursor + } + + i-- + character = if (i == 0) ' ' else text[i - 1] + } + + // maybe caught domain name? try search username + if (i > 2 && character == '@') { + var j = i - 1 + var character2 = text[i - 2] + + // again go up to first illegal character or tag "@" + while (j > 0 && character2 != '@') { + if (!isMentionOrHashtagAllowedCharacter(character2)) { + break + } + + j-- + character2 = if (j == 0) ' ' else text[j - 1] + } + + // found mention symbol, override cursor + if (character2 == '@') { + i = j + character = character2 + } + } + + if (i < 1 || + (character != '@' && character != '#' && character != ':') || + i > 1 && !Character.isWhitespace(text[i - 2]) + ) { + return cursor + } + return i - 1 + } + + override fun findTokenEnd(text: CharSequence, cursor: Int): Int { + var i = cursor + val length = text.length + while (i < length) { + if (text[i] == ' ') { + return i + } else { + i++ + } + } + return length + } + + override fun terminateToken(text: CharSequence): CharSequence { + var i = text.length + while (i > 0 && text[i - 1] == ' ') { + i-- + } + return if (i > 0 && text[i - 1] == ' ') { + text + } else if (text is Spanned) { + val s = SpannableString("$text ") + TextUtils.copySpansFrom(text, 0, text.length, Object::class.java, s, 0) + s + } else { + "$text " + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt new file mode 100644 index 0000000..755583c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -0,0 +1,600 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.compose + +import android.net.Uri +import android.util.Log +import androidx.core.net.toUri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold +import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeKind +import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia +import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter.AutocompleteResult +import com.keylesspalace.tusky.components.drafts.DraftHelper +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository +import com.keylesspalace.tusky.components.search.SearchType +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.NewPoll +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.service.MediaToSend +import com.keylesspalace.tusky.service.ServiceClient +import com.keylesspalace.tusky.service.StatusToSend +import com.keylesspalace.tusky.util.randomAlphanumericString +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext + +@HiltViewModel +class ComposeViewModel @Inject constructor( + private val api: MastodonApi, + private val accountManager: AccountManager, + private val mediaUploader: MediaUploader, + private val serviceClient: ServiceClient, + private val draftHelper: DraftHelper, + instanceInfoRepo: InstanceInfoRepository +) : ViewModel() { + + private var replyingStatusAuthor: String? = null + private var replyingStatusContent: String? = null + internal var startingText: String? = null + internal var postLanguage: String? = null + private var draftId: Int = 0 + private var scheduledTootId: String? = null + private var startingContentWarning: String = "" + private var inReplyToId: String? = null + private var originalStatusId: String? = null + private var startingVisibility: Status.Visibility = Status.Visibility.UNKNOWN + + private var contentWarningStateChanged: Boolean = false + private var modifiedInitialState: Boolean = false + private var hasScheduledTimeChanged: Boolean = false + + private var currentContent: String? = "" + private var currentContentWarning: String? = "" + + val instanceInfo: SharedFlow<InstanceInfo> = instanceInfoRepo::getUpdatedInstanceInfoOrFallback.asFlow() + .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) + + val emoji: SharedFlow<List<Emoji>> = instanceInfoRepo::getEmojis.asFlow() + .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) + + private val _markMediaAsSensitive = + MutableStateFlow(accountManager.activeAccount?.defaultMediaSensitivity ?: false) + val markMediaAsSensitive: StateFlow<Boolean> = _markMediaAsSensitive.asStateFlow() + + private val _statusVisibility = MutableStateFlow(Status.Visibility.UNKNOWN) + val statusVisibility: StateFlow<Status.Visibility> = _statusVisibility.asStateFlow() + + private val _showContentWarning = MutableStateFlow(false) + val showContentWarning: StateFlow<Boolean> = _showContentWarning.asStateFlow() + + private val _poll = MutableStateFlow(null as NewPoll?) + val poll: StateFlow<NewPoll?> = _poll.asStateFlow() + + private val _scheduledAt = MutableStateFlow(null as String?) + val scheduledAt: StateFlow<String?> = _scheduledAt.asStateFlow() + + private val _media = MutableStateFlow(emptyList<QueuedMedia>()) + val media: StateFlow<List<QueuedMedia>> = _media.asStateFlow() + + private val _uploadError = MutableSharedFlow<Throwable>( + replay = 0, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val uploadError: SharedFlow<Throwable> = _uploadError.asSharedFlow() + + private val _closeConfirmation = MutableStateFlow(ConfirmationKind.NONE) + val closeConfirmation: StateFlow<ConfirmationKind> = _closeConfirmation.asStateFlow() + + private lateinit var composeKind: ComposeKind + + // Used in ComposeActivity to pass state to result function when cropImage contract inflight + var cropImageItemOld: QueuedMedia? = null + + private var setupComplete = false + + suspend fun pickMedia( + mediaUri: Uri, + description: String? = null, + focus: Attachment.Focus? = null + ): Result<QueuedMedia> = withContext( + Dispatchers.IO + ) { + try { + val (type, uri, size) = mediaUploader.prepareMedia(mediaUri, instanceInfo.first()) + val mediaItems = _media.value + if (type != QueuedMedia.Type.IMAGE && + mediaItems.isNotEmpty() && + mediaItems[0].type == QueuedMedia.Type.IMAGE + ) { + Result.failure(VideoOrImageException()) + } else { + val queuedMedia = addMediaToQueue(type, uri, size, description, focus) + Result.success(queuedMedia) + } + } catch (e: Exception) { + Result.failure(e) + } + } + + suspend fun addMediaToQueue( + type: QueuedMedia.Type, + uri: Uri, + mediaSize: Long, + description: String? = null, + focus: Attachment.Focus? = null, + replaceItem: QueuedMedia? = null + ): QueuedMedia { + var stashMediaItem: QueuedMedia? = null + + _media.update { mediaList -> + val mediaItem = QueuedMedia( + localId = mediaUploader.getNewLocalMediaId(), + uri = uri, + type = type, + mediaSize = mediaSize, + description = description, + focus = focus, + state = QueuedMedia.State.UPLOADING + ) + stashMediaItem = mediaItem + + if (replaceItem != null) { + mediaUploader.cancelUploadScope(replaceItem.localId) + mediaList.map { + if (it.localId == replaceItem.localId) mediaItem else it + } + } else { // Append + mediaList + mediaItem + } + } + val mediaItem = + stashMediaItem!! // stashMediaItem is always non-null and uncaptured at this point, but Kotlin doesn't know that + + viewModelScope.launch { + mediaUploader + .uploadMedia(mediaItem, instanceInfo.first()) + .collect { event -> + val item = _media.value.find { it.localId == mediaItem.localId } + ?: return@collect + val newMediaItem = when (event) { + is UploadEvent.ProgressEvent -> + item.copy(uploadPercent = event.percentage) + + is UploadEvent.FinishedEvent -> + item.copy( + id = event.mediaId, + uploadPercent = -1, + state = if (event.processed) { + QueuedMedia.State.PROCESSED + } else { + QueuedMedia.State.UNPROCESSED + } + ) + is UploadEvent.ErrorEvent -> { + _media.update { mediaList -> mediaList.filter { it.localId != mediaItem.localId } } + _uploadError.emit(event.error) + return@collect + } + } + _media.update { mediaList -> + mediaList.map { mediaItem -> + if (mediaItem.localId == newMediaItem.localId) { + newMediaItem + } else { + mediaItem + } + } + } + } + } + updateCloseConfirmation() + return mediaItem + } + + fun changeStatusVisibility(visibility: Status.Visibility) { + _statusVisibility.value = visibility + } + + private fun addUploadedMedia( + id: String, + type: QueuedMedia.Type, + uri: Uri, + description: String?, + focus: Attachment.Focus? + ) { + _media.update { mediaList -> + val mediaItem = QueuedMedia( + localId = mediaUploader.getNewLocalMediaId(), + uri = uri, + type = type, + mediaSize = 0, + uploadPercent = -1, + id = id, + description = description, + focus = focus, + state = QueuedMedia.State.PUBLISHED + ) + mediaList + mediaItem + } + } + + fun removeMediaFromQueue(item: QueuedMedia) { + mediaUploader.cancelUploadScope(item.localId) + _media.update { mediaList -> mediaList.filter { it.localId != item.localId } } + updateCloseConfirmation() + } + + fun toggleMarkSensitive() { + this._markMediaAsSensitive.value = this._markMediaAsSensitive.value != true + } + + fun updateContent(newContent: String?) { + currentContent = newContent + updateCloseConfirmation() + } + + fun updateContentWarning(newContentWarning: String?) { + currentContentWarning = newContentWarning + updateCloseConfirmation() + } + + private fun updateCloseConfirmation() { + val contentWarning = if (_showContentWarning.value) { + currentContentWarning + } else { + "" + } + this._closeConfirmation.value = if (didChange(currentContent, contentWarning)) { + when (composeKind) { + ComposeKind.NEW -> if (isEmpty(currentContent, contentWarning)) { + ConfirmationKind.NONE + } else { + ConfirmationKind.SAVE_OR_DISCARD + } + ComposeKind.EDIT_DRAFT -> if (isEmpty(currentContent, contentWarning)) { + ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_DRAFT + } else { + ConfirmationKind.UPDATE_OR_DISCARD + } + ComposeKind.EDIT_POSTED -> ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_CHANGES + ComposeKind.EDIT_SCHEDULED -> ConfirmationKind.CONTINUE_EDITING_OR_DISCARD_CHANGES + } + } else { + ConfirmationKind.NONE + } + } + + private fun didChange(content: String?, contentWarning: String?): Boolean { + val textChanged = content.orEmpty() != startingText.orEmpty() + val contentWarningChanged = contentWarning.orEmpty() != startingContentWarning + val mediaChanged = _media.value.isNotEmpty() + val pollChanged = _poll.value != null + val didScheduledTimeChange = hasScheduledTimeChanged + + return modifiedInitialState || textChanged || contentWarningChanged || mediaChanged || pollChanged || didScheduledTimeChange + } + + private fun isEmpty(content: String?, contentWarning: String?): Boolean { + return !modifiedInitialState && (content.isNullOrBlank() && contentWarning.isNullOrBlank() && _media.value.isEmpty() && _poll.value == null) + } + + fun contentWarningChanged(value: Boolean) { + _showContentWarning.value = value + contentWarningStateChanged = true + updateCloseConfirmation() + } + + fun deleteDraft() { + viewModelScope.launch { + if (draftId != 0) { + draftHelper.deleteDraftAndAttachments(draftId) + } + } + } + + fun stopUploads() { + mediaUploader.cancelUploadScope(*_media.value.map { it.localId }.toIntArray()) + } + + fun shouldShowSaveDraftDialog(): Boolean { + // if any of the media files need to be downloaded first it could take a while, so show a loading dialog + return _media.value.any { mediaValue -> + mediaValue.uri.scheme == "https" + } + } + + suspend fun saveDraft(content: String, contentWarning: String) { + val mediaUris: MutableList<String> = mutableListOf() + val mediaDescriptions: MutableList<String?> = mutableListOf() + val mediaFocus: MutableList<Attachment.Focus?> = mutableListOf() + for (item in _media.value) { + mediaUris.add(item.uri.toString()) + mediaDescriptions.add(item.description) + mediaFocus.add(item.focus) + } + + draftHelper.saveDraft( + draftId = draftId, + accountId = accountManager.activeAccount?.id!!, + inReplyToId = inReplyToId, + content = content, + contentWarning = contentWarning, + sensitive = _markMediaAsSensitive.value, + visibility = _statusVisibility.value, + mediaUris = mediaUris, + mediaDescriptions = mediaDescriptions, + mediaFocus = mediaFocus, + poll = _poll.value, + failedToSend = false, + failedToSendAlert = false, + scheduledAt = _scheduledAt.value, + language = postLanguage, + statusId = originalStatusId + ) + } + + /** + * Send status to the server. + * Uses current state plus provided arguments. + */ + suspend fun sendStatus(content: String, spoilerText: String, accountId: Long) { + if (!scheduledTootId.isNullOrEmpty()) { + api.deleteScheduledStatus(scheduledTootId!!) + } + + val attachedMedia = _media.value.map { item -> + MediaToSend( + localId = item.localId, + id = item.id, + uri = item.uri.toString(), + description = item.description, + focus = item.focus, + processed = item.state == QueuedMedia.State.PROCESSED || item.state == QueuedMedia.State.PUBLISHED + ) + } + val tootToSend = StatusToSend( + text = content, + warningText = spoilerText, + visibility = _statusVisibility.value.serverString, + sensitive = attachedMedia.isNotEmpty() && (_markMediaAsSensitive.value || _showContentWarning.value), + media = attachedMedia, + scheduledAt = _scheduledAt.value, + inReplyToId = inReplyToId, + poll = _poll.value, + replyingStatusContent = null, + replyingStatusAuthorUsername = null, + accountId = accountId, + draftId = draftId, + idempotencyKey = randomAlphanumericString(16), + retries = 0, + language = postLanguage, + statusId = originalStatusId + ) + + serviceClient.sendToot(tootToSend) + } + + private fun updateMediaItem(localId: Int, mutator: (QueuedMedia) -> QueuedMedia) { + _media.update { mediaList -> + mediaList.map { mediaItem -> + if (mediaItem.localId == localId) { + mutator(mediaItem) + } else { + mediaItem + } + } + } + } + + fun updateDescription(localId: Int, description: String) { + updateMediaItem(localId) { mediaItem -> + mediaItem.copy(description = description) + } + } + + fun updateFocus(localId: Int, focus: Attachment.Focus) { + updateMediaItem(localId) { mediaItem -> + mediaItem.copy(focus = focus) + } + } + + fun searchAutocompleteSuggestions(token: String): List<AutocompleteResult> { + return when (token[0]) { + '@' -> runBlocking { + api.searchAccounts(query = token.substring(1), limit = 10) + .fold({ accounts -> + accounts.map { AutocompleteResult.AccountResult(it) } + }, { e -> + Log.e(TAG, "Autocomplete search for $token failed.", e) + emptyList() + }) + } + '#' -> runBlocking { + api.search( + query = token, + type = SearchType.Hashtag.apiParameter, + limit = 10 + ) + .fold({ searchResult -> + searchResult.hashtags.map { AutocompleteResult.HashtagResult(it.name) } + }, { e -> + Log.e(TAG, "Autocomplete search for $token failed.", e) + emptyList() + }) + } + + ':' -> { + val emojiList = emoji.replayCache.firstOrNull() ?: return emptyList() + val incomplete = token.substring(1) + + emojiList.filter { emoji -> + emoji.shortcode.contains(incomplete, ignoreCase = true) + }.sortedBy { emoji -> + emoji.shortcode.indexOf(incomplete, ignoreCase = true) + }.map { emoji -> + AutocompleteResult.EmojiResult(emoji) + } + } + + else -> { + Log.w(TAG, "Unexpected autocompletion token: $token") + emptyList() + } + } + } + + fun setup(composeOptions: ComposeActivity.ComposeOptions?) { + if (setupComplete) { + return + } + + composeKind = composeOptions?.kind ?: ComposeKind.NEW + inReplyToId = composeOptions?.inReplyToId + + val activeAccount = accountManager.activeAccount!! + val preferredVisibility = + if (inReplyToId != null) activeAccount.defaultReplyPrivacy else activeAccount.defaultPostPrivacy + + val replyVisibility = composeOptions?.replyVisibility ?: Status.Visibility.UNKNOWN + startingVisibility = Status.Visibility.byNum( + preferredVisibility.num.coerceAtLeast(replyVisibility.num) + ) + + modifiedInitialState = composeOptions?.modifiedInitialState == true + + val contentWarning = composeOptions?.contentWarning + if (contentWarning != null) { + startingContentWarning = contentWarning + } + if (!contentWarningStateChanged) { + _showContentWarning.value = !contentWarning.isNullOrBlank() + } + + // recreate media list + val draftAttachments = composeOptions?.draftAttachments + if (draftAttachments != null) { + // when coming from DraftActivity + viewModelScope.launch { + draftAttachments.forEach { attachment -> + pickMedia(attachment.uri, attachment.description, attachment.focus) + } + } + } else { + composeOptions?.mediaAttachments?.forEach { a -> + // when coming from redraft or ScheduledTootActivity + val mediaType = when (a.type) { + Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO + Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE + Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO + } + addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description, a.meta?.focus) + } + } + + draftId = composeOptions?.draftId ?: 0 + scheduledTootId = composeOptions?.scheduledTootId + originalStatusId = composeOptions?.statusId + startingText = composeOptions?.content + currentContent = composeOptions?.content + postLanguage = composeOptions?.language + + val tootVisibility = composeOptions?.visibility ?: Status.Visibility.UNKNOWN + if (tootVisibility.num != Status.Visibility.UNKNOWN.num) { + startingVisibility = tootVisibility + } + _statusVisibility.value = startingVisibility + val mentionedUsernames = composeOptions?.mentionedUsernames + if (mentionedUsernames != null) { + val builder = StringBuilder() + for (name in mentionedUsernames) { + builder.append('@') + builder.append(name) + builder.append(' ') + } + startingText = builder.toString() + } + + _scheduledAt.value = composeOptions?.scheduledAt + + composeOptions?.sensitive?.let { _markMediaAsSensitive.value = it } + + val poll = composeOptions?.poll + if (poll != null && composeOptions.mediaAttachments.isNullOrEmpty()) { + this._poll.value = poll + } + replyingStatusContent = composeOptions?.replyingStatusContent + replyingStatusAuthor = composeOptions?.replyingStatusAuthor + + updateCloseConfirmation() + + setupComplete = true + } + + fun updatePoll(newPoll: NewPoll?) { + _poll.value = newPoll + updateCloseConfirmation() + } + + fun updateScheduledAt(newScheduledAt: String?) { + if (newScheduledAt != _scheduledAt.value) { + hasScheduledTimeChanged = true + } + + _scheduledAt.value = newScheduledAt + } + + val editing: Boolean + get() = !originalStatusId.isNullOrEmpty() + + private companion object { + const val TAG = "ComposeViewModel" + } + + enum class ConfirmationKind { + NONE, // just close + SAVE_OR_DISCARD, + UPDATE_OR_DISCARD, + CONTINUE_EDITING_OR_DISCARD_CHANGES, // editing post + CONTINUE_EDITING_OR_DISCARD_DRAFT // edit draft + } +} + +/** + * Thrown when trying to add an image when video is already present or the other way around + */ +class VideoOrImageException : Exception() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ImageDownsizer.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ImageDownsizer.kt new file mode 100644 index 0000000..fd355ca --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ImageDownsizer.kt @@ -0,0 +1,100 @@ +/* Copyright 2022 Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.compose + +import android.content.ContentResolver +import android.graphics.Bitmap +import android.graphics.Bitmap.CompressFormat +import android.graphics.BitmapFactory +import android.net.Uri +import com.keylesspalace.tusky.util.calculateInSampleSize +import com.keylesspalace.tusky.util.closeQuietly +import com.keylesspalace.tusky.util.getImageOrientation +import com.keylesspalace.tusky.util.reorientBitmap +import java.io.File +import java.io.FileNotFoundException +import java.io.FileOutputStream + +/** + * @param uri the uri pointing to the input file + * @param sizeLimit the maximum number of bytes the output image is allowed to have + * @param contentResolver to resolve the specified input uri + * @param tempFile the file where the result will be stored + * @return true when the image was successfully resized, false otherwise + */ +fun downsizeImage( + uri: Uri, + sizeLimit: Int, + contentResolver: ContentResolver, + tempFile: File +): Boolean { + val decodeBoundsInputStream = try { + contentResolver.openInputStream(uri) ?: return false + } catch (e: FileNotFoundException) { + return false + } + // Initially, just get the image dimensions. + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + BitmapFactory.decodeStream(decodeBoundsInputStream, null, options) + decodeBoundsInputStream.closeQuietly() + // Get EXIF data, for orientation info. + val orientation = getImageOrientation(uri, contentResolver) + /* Unfortunately, there isn't a determined worst case compression ratio for image + * formats. So, the only way to tell if they're too big is to compress them and + * test, and keep trying at smaller sizes. The initial estimate should be good for + * many cases, so it should only iterate once, but the loop is used to be absolutely + * sure it gets downsized to below the limit. */ + var scaledImageSize = 1024 + do { + val outputStream = try { + FileOutputStream(tempFile) + } catch (e: FileNotFoundException) { + return false + } + val decodeBitmapInputStream = try { + contentResolver.openInputStream(uri) ?: return false + } catch (e: FileNotFoundException) { + return false + } + options.inSampleSize = calculateInSampleSize(options, scaledImageSize, scaledImageSize) + options.inJustDecodeBounds = false + val scaledBitmap: Bitmap = try { + BitmapFactory.decodeStream(decodeBitmapInputStream, null, options) + } catch (error: OutOfMemoryError) { + return false + } finally { + decodeBitmapInputStream.closeQuietly() + } ?: return false + + val reorientedBitmap = reorientBitmap(scaledBitmap, orientation) + if (reorientedBitmap == null) { + scaledBitmap.recycle() + return false + } + /* Retain transparency if there is any by encoding as png */ + val format: CompressFormat = if (!reorientedBitmap.hasAlpha()) { + CompressFormat.JPEG + } else { + CompressFormat.PNG + } + reorientedBitmap.compress(format, 85, outputStream) + reorientedBitmap.recycle() + scaledImageSize /= 2 + } while (tempFile.length() > sizeLimit) + + return true +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt new file mode 100644 index 0000000..5837405 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt @@ -0,0 +1,135 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.compose + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.PopupMenu +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.compose.view.ProgressImageView + +class MediaPreviewAdapter( + context: Context, + private val onAddCaption: (ComposeActivity.QueuedMedia) -> Unit, + private val onAddFocus: (ComposeActivity.QueuedMedia) -> Unit, + private val onEditImage: (ComposeActivity.QueuedMedia) -> Unit, + private val onRemove: (ComposeActivity.QueuedMedia) -> Unit +) : ListAdapter<ComposeActivity.QueuedMedia, MediaPreviewAdapter.PreviewViewHolder>( + object : DiffUtil.ItemCallback<ComposeActivity.QueuedMedia>() { + override fun areItemsTheSame( + oldItem: ComposeActivity.QueuedMedia, + newItem: ComposeActivity.QueuedMedia + ) = oldItem.localId == newItem.localId + + override fun areContentsTheSame( + oldItem: ComposeActivity.QueuedMedia, + newItem: ComposeActivity.QueuedMedia + ) = oldItem == newItem + } +) { + + private fun onMediaClick(item: ComposeActivity.QueuedMedia, view: View) { + val popup = PopupMenu(view.context, view) + val addCaptionId = 1 + val addFocusId = 2 + val editImageId = 3 + val removeId = 4 + + popup.menu.add(0, addCaptionId, 0, R.string.action_set_caption) + if (item.type == ComposeActivity.QueuedMedia.Type.IMAGE) { + popup.menu.add(0, addFocusId, 0, R.string.action_set_focus) + if (item.state != ComposeActivity.QueuedMedia.State.PUBLISHED) { + // Already-published items can't be edited + popup.menu.add(0, editImageId, 0, R.string.action_edit_image) + } + } + popup.menu.add(0, removeId, 0, R.string.action_remove) + popup.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + addCaptionId -> onAddCaption(item) + addFocusId -> onAddFocus(item) + editImageId -> onEditImage(item) + removeId -> onRemove(item) + } + true + } + popup.show() + } + + private val thumbnailViewSize = + context.resources.getDimensionPixelSize(R.dimen.compose_media_preview_size) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PreviewViewHolder { + return PreviewViewHolder(ProgressImageView(parent.context)) + } + + override fun onBindViewHolder(holder: PreviewViewHolder, position: Int) { + val item = getItem(position) + holder.progressImageView.setChecked(!item.description.isNullOrEmpty()) + holder.progressImageView.setProgress(item.uploadPercent) + if (item.type == ComposeActivity.QueuedMedia.Type.AUDIO) { + // TODO: Fancy waveform display? + holder.progressImageView.setImageResource(R.drawable.ic_music_box_preview_24dp) + } else { + val imageView = holder.progressImageView + val focus = item.focus + + if (focus != null) { + imageView.setFocalPoint(focus) + } else { + imageView.removeFocalPoint() // Probably unnecessary since we have no UI for removal once added. + } + + var glide = Glide.with(holder.itemView.context) + .load(item.uri) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .dontAnimate() + .centerInside() + + if (focus != null) { + glide = glide.addListener(imageView) + } + + glide.into(imageView) + + holder.progressImageView.setOnClickListener { + onMediaClick(item, holder.progressImageView) + } + } + } + + inner class PreviewViewHolder(val progressImageView: ProgressImageView) : + RecyclerView.ViewHolder(progressImageView) { + init { + val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize) + val margin = itemView.context.resources + .getDimensionPixelSize(R.dimen.compose_media_preview_margin) + val marginBottom = itemView.context.resources + .getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom) + layoutParams.setMargins(margin, 0, margin, marginBottom) + progressImageView.layoutParams = layoutParams + progressImageView.scaleType = ImageView.ScaleType.CENTER_CROP + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt new file mode 100644 index 0000000..9c5520c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt @@ -0,0 +1,335 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.compose + +import android.content.ContentResolver +import android.content.Context +import android.media.MediaMetadataRetriever +import android.media.MediaMetadataRetriever.METADATA_KEY_MIMETYPE +import android.net.Uri +import android.os.Environment +import android.util.Log +import android.webkit.MimeTypeMap +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.compose.ComposeActivity.QueuedMedia +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo +import com.keylesspalace.tusky.network.MediaUploadApi +import com.keylesspalace.tusky.network.asRequestBody +import com.keylesspalace.tusky.util.MEDIA_SIZE_UNKNOWN +import com.keylesspalace.tusky.util.getImageSquarePixels +import com.keylesspalace.tusky.util.getMediaSize +import com.keylesspalace.tusky.util.getServerErrorMessage +import com.keylesspalace.tusky.util.randomAlphanumericString +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.File +import java.io.IOException +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.cancel +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.shareIn +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okio.buffer +import okio.sink +import okio.source +import retrofit2.HttpException + +sealed interface FinalUploadEvent + +sealed interface UploadEvent { + data class ProgressEvent(val percentage: Int) : UploadEvent + data class FinishedEvent( + val mediaId: String, + val processed: Boolean + ) : UploadEvent, FinalUploadEvent + data class ErrorEvent(val error: Throwable) : UploadEvent, FinalUploadEvent +} + +data class UploadData( + val flow: Flow<UploadEvent>, + val scope: CoroutineScope +) + +fun createNewImageFile(context: Context, suffix: String = ".jpg"): File { + // Create an image file name + val randomId = randomAlphanumericString(12) + val imageFileName = "Tusky_${randomId}_" + val storageDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + return File.createTempFile(imageFileName, suffix, storageDir) +} + +data class PreparedMedia(val type: QueuedMedia.Type, val uri: Uri, val size: Long) + +class FileSizeException(val allowedSizeInBytes: Int) : Exception() +class MediaTypeException : Exception() +class CouldNotOpenFileException : Exception() +class UploadServerError(val errorMessage: String) : Exception() + +@Singleton +class MediaUploader @Inject constructor( + @ApplicationContext private val context: Context, + private val mediaUploadApi: MediaUploadApi +) { + + private val uploads = mutableMapOf<Int, UploadData>() + + private var mostRecentId: Int = 0 + + fun getNewLocalMediaId(): Int { + return mostRecentId++ + } + + suspend fun getMediaUploadState(localId: Int): FinalUploadEvent { + return uploads[localId]?.flow + ?.filterIsInstance<FinalUploadEvent>() + ?.first() + ?: UploadEvent.ErrorEvent(IllegalStateException("media upload with id $localId not found")) + } + + /** + * Uploads media. + * @param media the media to upload + * @param instanceInfo info about the current media to make sure the media gets resized correctly + * @return A Flow emitting upload events. + * The Flow is hot, in order to cancel upload or clear resources call [cancelUploadScope]. + */ + @OptIn(ExperimentalCoroutinesApi::class) + fun uploadMedia(media: QueuedMedia, instanceInfo: InstanceInfo): Flow<UploadEvent> { + val uploadScope = CoroutineScope(Dispatchers.IO) + val uploadFlow = flow { + if (shouldResizeMedia(media, instanceInfo)) { + emit(downsize(media, instanceInfo)) + } else { + emit(media) + } + } + .flatMapLatest { upload(it) } + .catch { exception -> + emit(UploadEvent.ErrorEvent(exception)) + } + .shareIn(uploadScope, SharingStarted.Lazily, 1) + + uploads[media.localId] = UploadData(uploadFlow, uploadScope) + return uploadFlow + } + + /** + * Cancels the CoroutineScope of a media upload. + * Call this when to abort the upload or to clean up resources after upload info is no longer needed + */ + fun cancelUploadScope(vararg localMediaIds: Int) { + localMediaIds.forEach { localId -> + uploads.remove(localId)?.scope?.cancel() + } + } + + fun prepareMedia(inUri: Uri, instanceInfo: InstanceInfo): PreparedMedia { + var mediaSize = MEDIA_SIZE_UNKNOWN + var uri = inUri + val mimeType: String? + + try { + when (inUri.scheme) { + ContentResolver.SCHEME_CONTENT -> { + mimeType = contentResolver.getType(uri) + + val suffix = "." + MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType ?: "tmp") + + contentResolver.openInputStream(inUri)?.source().use { input -> + if (input == null) { + Log.w(TAG, "Media input is null") + uri = inUri + return@use + } + val file = File.createTempFile("randomTemp1", suffix, context.cacheDir) + file.absoluteFile.sink().buffer().use { out -> + out.writeAll(input) + } + uri = FileProvider.getUriForFile( + context, + BuildConfig.APPLICATION_ID + ".fileprovider", + file + ) + mediaSize = getMediaSize(contentResolver, uri) + } + } + ContentResolver.SCHEME_FILE -> { + val path = uri.path + if (path == null) { + Log.w(TAG, "empty uri path $uri") + throw CouldNotOpenFileException() + } + val inputFile = File(path) + val suffix = inputFile.name.substringAfterLast('.', "tmp") + mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(suffix) + val file = File.createTempFile("randomTemp1", ".$suffix", context.cacheDir) + + inputFile.source().use { input -> + file.absoluteFile.sink().buffer().use { out -> + out.writeAll(input) + } + } + uri = FileProvider.getUriForFile( + context, + BuildConfig.APPLICATION_ID + ".fileprovider", + file + ) + mediaSize = getMediaSize(contentResolver, uri) + } + else -> { + Log.w(TAG, "Unknown uri scheme $uri") + throw CouldNotOpenFileException() + } + } + } catch (e: IOException) { + Log.w(TAG, e) + throw CouldNotOpenFileException() + } + if (mediaSize == MEDIA_SIZE_UNKNOWN) { + Log.w(TAG, "Could not determine file size of upload") + throw MediaTypeException() + } + + if (mimeType != null) { + return when (mimeType.substring(0, mimeType.indexOf('/'))) { + "video" -> { + if (mediaSize > instanceInfo.videoSizeLimit) { + throw FileSizeException(instanceInfo.videoSizeLimit) + } + PreparedMedia(QueuedMedia.Type.VIDEO, uri, mediaSize) + } + "image" -> { + PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize) + } + "audio" -> { + if (mediaSize > instanceInfo.videoSizeLimit) { + throw FileSizeException(instanceInfo.videoSizeLimit) + } + PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize) + } + else -> { + throw MediaTypeException() + } + } + } else { + Log.w(TAG, "Could not determine mime type of upload") + throw MediaTypeException() + } + } + + private val contentResolver = context.contentResolver + + private suspend fun upload(media: QueuedMedia): Flow<UploadEvent> { + return callbackFlow { + var mimeType = contentResolver.getType(media.uri) + + // Android's MIME type suggestions from file extensions is broken for at least + // .m4a files. See https://github.com/tuskyapp/Tusky/issues/3189 for details. + // Sniff the content of the file to determine the actual type. + if (mimeType != null && ( + mimeType.startsWith("audio/", ignoreCase = true) || + mimeType.startsWith("video/", ignoreCase = true) + ) + ) { + val retriever = MediaMetadataRetriever() + retriever.setDataSource(context, media.uri) + mimeType = retriever.extractMetadata(METADATA_KEY_MIMETYPE) + } + val map = MimeTypeMap.getSingleton() + val fileExtension = map.getExtensionFromMimeType(mimeType) + val filename = "%s_%d_%s.%s".format( + context.getString(R.string.app_name), + System.currentTimeMillis(), + randomAlphanumericString(10), + fileExtension + ) + + if (mimeType == null) mimeType = "multipart/form-data" + + var lastProgress = -1 + val fileBody = media.uri.asRequestBody( + contentResolver, + requireNotNull(mimeType.toMediaTypeOrNull()) { "Invalid Content Type" }, + media.mediaSize + ) { percentage -> + if (percentage != lastProgress) { + trySend(UploadEvent.ProgressEvent(percentage)) + } + lastProgress = percentage + } + + val body = MultipartBody.Part.createFormData("file", filename, fileBody) + + val description = if (media.description != null) { + MultipartBody.Part.createFormData("description", media.description) + } else { + null + } + + val focus = if (media.focus != null) { + MultipartBody.Part.createFormData("focus", "${media.focus.x},${media.focus.y}") + } else { + null + } + + val uploadResponse = mediaUploadApi.uploadMedia(body, description, focus) + val responseBody = uploadResponse.body() + if (uploadResponse.isSuccessful && responseBody != null) { + send(UploadEvent.FinishedEvent(responseBody.id, uploadResponse.code() == 200)) + } else { + val error = HttpException(uploadResponse) + val errorMessage = error.getServerErrorMessage() + if (errorMessage == null) { + throw error + } else { + throw UploadServerError(errorMessage) + } + } + + awaitClose() + } + } + + private fun downsize(media: QueuedMedia, instanceInfo: InstanceInfo): QueuedMedia { + val file = createNewImageFile(context) + downsizeImage(media.uri, instanceInfo.imageSizeLimit, contentResolver, file) + return media.copy(uri = file.toUri(), mediaSize = file.length()) + } + + private fun shouldResizeMedia(media: QueuedMedia, instanceInfo: InstanceInfo): Boolean { + return media.type == QueuedMedia.Type.IMAGE && + (media.mediaSize > instanceInfo.imageSizeLimit || getImageSquarePixels(context.contentResolver, media.uri) > instanceInfo.imageMatrixLimit) + } + + private companion object { + private const val TAG = "MediaUploader" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt new file mode 100644 index 0000000..1d8168d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollDialog.kt @@ -0,0 +1,113 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +@file:JvmName("AddPollDialog") + +package com.keylesspalace.tusky.components.compose.dialog + +import android.content.Context +import android.view.LayoutInflater +import android.view.WindowManager +import android.widget.ArrayAdapter +import androidx.appcompat.app.AlertDialog +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.DialogAddPollBinding +import com.keylesspalace.tusky.entity.NewPoll + +fun showAddPollDialog( + context: Context, + poll: NewPoll?, + maxOptionCount: Int, + maxOptionLength: Int, + minDuration: Int, + maxDuration: Int, + onUpdatePoll: (NewPoll) -> Unit +) { + val binding = DialogAddPollBinding.inflate(LayoutInflater.from(context)) + + val dialog = AlertDialog.Builder(context) + .setIcon(R.drawable.ic_poll_24dp) + .setTitle(R.string.create_poll_title) + .setView(binding.root) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, null) + .create() + + val adapter = AddPollOptionsAdapter( + options = poll?.options?.toMutableList() ?: mutableListOf("", ""), + maxOptionLength = maxOptionLength, + onOptionRemoved = { valid -> + binding.addChoiceButton.isEnabled = true + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid + }, + onOptionChanged = { valid -> + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = valid + } + ) + + binding.pollChoices.adapter = adapter + + var durations = context.resources.getIntArray(R.array.poll_duration_values).toList() + val durationLabels = context.resources.getStringArray( + R.array.poll_duration_names + ).filterIndexed { index, _ -> durations[index] in minDuration..maxDuration } + binding.pollDurationSpinner.adapter = ArrayAdapter(context, android.R.layout.simple_spinner_item, durationLabels).apply { + setDropDownViewResource(androidx.appcompat.R.layout.support_simple_spinner_dropdown_item) + } + durations = durations.filter { it in minDuration..maxDuration } + + binding.addChoiceButton.setOnClickListener { + if (adapter.itemCount < maxOptionCount) { + adapter.addChoice() + } + if (adapter.itemCount >= maxOptionCount) { + it.isEnabled = false + } + } + + val secondsInADay = 60 * 60 * 24 + val desiredDuration = poll?.expiresIn ?: secondsInADay + val pollDurationId = durations.indexOfLast { + it <= desiredDuration + } + + binding.pollDurationSpinner.setSelection(pollDurationId) + + binding.multipleChoicesCheckBox.isChecked = poll?.multiple ?: false + + dialog.setOnShowListener { + val button = dialog.getButton(AlertDialog.BUTTON_POSITIVE) + button.setOnClickListener { + val selectedPollDurationId = binding.pollDurationSpinner.selectedItemPosition + + onUpdatePoll( + NewPoll( + options = adapter.pollOptions, + expiresIn = durations[selectedPollDurationId], + multiple = binding.multipleChoicesCheckBox.isChecked + ) + ) + + dialog.dismiss() + } + } + + dialog.show() + + // make the dialog focusable so the keyboard does not stay behind it + dialog.window?.clearFlags( + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt new file mode 100644 index 0000000..ad7f7db --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/AddPollOptionsAdapter.kt @@ -0,0 +1,87 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.compose.dialog + +import android.text.InputFilter +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.widget.doOnTextChanged +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemAddPollOptionBinding +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.visible + +class AddPollOptionsAdapter( + private var options: MutableList<String>, + private val maxOptionLength: Int, + private val onOptionRemoved: (Boolean) -> Unit, + private val onOptionChanged: (Boolean) -> Unit +) : RecyclerView.Adapter<BindingHolder<ItemAddPollOptionBinding>>() { + + val pollOptions: List<String> + get() = options.toList() + + fun addChoice() { + options.add("") + notifyItemInserted(options.size - 1) + } + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemAddPollOptionBinding> { + val binding = ItemAddPollOptionBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + val holder = BindingHolder(binding) + binding.optionEditText.filters = arrayOf(InputFilter.LengthFilter(maxOptionLength)) + + binding.optionEditText.doOnTextChanged { s, _, _, _ -> + val pos = holder.bindingAdapterPosition + if (pos != RecyclerView.NO_POSITION) { + options[pos] = s.toString() + onOptionChanged(validateInput()) + } + } + + return holder + } + + override fun getItemCount() = options.size + + override fun onBindViewHolder(holder: BindingHolder<ItemAddPollOptionBinding>, position: Int) { + holder.binding.optionEditText.setText(options[position]) + + holder.binding.optionTextInputLayout.hint = holder.binding.root.context.getString(R.string.poll_new_choice_hint, position + 1) + + holder.binding.deleteButton.visible(position > 1, View.INVISIBLE) + + holder.binding.deleteButton.setOnClickListener { + holder.binding.optionEditText.clearFocus() + options.removeAt(holder.bindingAdapterPosition) + notifyItemRemoved(holder.bindingAdapterPosition) + onOptionRemoved(validateInput()) + } + } + + private fun validateInput(): Boolean { + return !(options.contains("") || options.distinct().size != options.size) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt new file mode 100644 index 0000000..d78009a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt @@ -0,0 +1,150 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.compose.dialog + +import android.content.Context +import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.Bundle +import android.text.InputFilter +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.view.WindowManager +import android.widget.LinearLayout +import androidx.core.os.bundleOf +import androidx.fragment.app.DialogFragment +import com.bumptech.glide.Glide +import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.DialogImageDescriptionBinding +import com.keylesspalace.tusky.util.getParcelableCompat +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.viewBinding + +// https://github.com/tootsuite/mastodon/blob/c6904c0d3766a2ea8a81ab025c127169ecb51373/app/models/media_attachment.rb#L32 +private const val MEDIA_DESCRIPTION_CHARACTER_LIMIT = 1500 + +class CaptionDialog : DialogFragment() { + private lateinit var listener: Listener + + private val binding by viewBinding(DialogImageDescriptionBinding::bind) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NORMAL, R.style.TuskyDialogFragmentStyle) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View = inflater.inflate(R.layout.dialog_image_description, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val imageView = binding.imageDescriptionView + imageView.maxZoom = 6f + + binding.imageDescriptionText.hint = resources.getQuantityString( + R.plurals.hint_describe_for_visually_impaired, + MEDIA_DESCRIPTION_CHARACTER_LIMIT, + MEDIA_DESCRIPTION_CHARACTER_LIMIT + ) + binding.imageDescriptionText.filters = arrayOf(InputFilter.LengthFilter(MEDIA_DESCRIPTION_CHARACTER_LIMIT)) + binding.imageDescriptionText.setText(arguments?.getString(EXISTING_DESCRIPTION_ARG)) + savedInstanceState?.getCharSequence(DESCRIPTION_KEY)?.let { + binding.imageDescriptionText.setText(it) + } + + binding.cancelButton.setOnClickListener { + dismiss() + } + val localId = arguments?.getInt(LOCAL_ID_ARG) ?: error("Missing localId") + binding.okButton.setOnClickListener { + listener.onUpdateDescription(localId, binding.imageDescriptionText.text.toString()) + dismiss() + } + + isCancelable = true + + val previewUri = arguments?.getParcelableCompat<Uri>(PREVIEW_URI_ARG) ?: error("Preview Uri is null") + + // Load the image and manually set it into the ImageView because it doesn't have a fixed size. + Glide.with(this) + .load(previewUri) + .downsample(DownsampleStrategy.CENTER_INSIDE) + .into(object : CustomTarget<Drawable>(4096, 4096) { + override fun onLoadCleared(placeholder: Drawable?) { + imageView.setImageDrawable(placeholder) + } + + override fun onResourceReady( + resource: Drawable, + transition: Transition<in Drawable>? + ) { + imageView.setImageDrawable(resource) + } + + override fun onLoadFailed(errorDrawable: Drawable?) { + super.onLoadFailed(errorDrawable) + imageView.hide() + } + }) + } + + override fun onStart() { + super.onStart() + dialog?.apply { + window?.setLayout( + LinearLayout.LayoutParams.MATCH_PARENT, + LinearLayout.LayoutParams.MATCH_PARENT + ) + window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE) + } + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putCharSequence(DESCRIPTION_KEY, binding.imageDescriptionText.text) + super.onSaveInstanceState(outState) + } + + override fun onAttach(context: Context) { + super.onAttach(context) + listener = context as? Listener ?: error("Activity is not ComposeCaptionDialog.Listener") + } + + interface Listener { + fun onUpdateDescription(localId: Int, description: String) + } + + companion object { + fun newInstance(localId: Int, existingDescription: String?, previewUri: Uri) = + CaptionDialog().apply { + arguments = bundleOf( + LOCAL_ID_ARG to localId, + EXISTING_DESCRIPTION_ARG to existingDescription, + PREVIEW_URI_ARG to previewUri + ) + } + + private const val DESCRIPTION_KEY = "description" + private const val EXISTING_DESCRIPTION_ARG = "existing_description" + private const val PREVIEW_URI_ARG = "preview_uri" + private const val LOCAL_ID_ARG = "local_id" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt new file mode 100644 index 0000000..6cfbeed --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/FocusDialog.kt @@ -0,0 +1,108 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.compose.dialog + +import android.content.DialogInterface +import android.graphics.drawable.Drawable +import android.net.Uri +import android.view.WindowManager +import android.widget.FrameLayout +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.load.resource.bitmap.DownsampleStrategy +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.keylesspalace.tusky.databinding.DialogFocusBinding +import com.keylesspalace.tusky.entity.Attachment.Focus +import kotlinx.coroutines.launch + +fun <T> T.makeFocusDialog( + existingFocus: Focus?, + previewUri: Uri, + onUpdateFocus: suspend (Focus) -> Unit +) where T : AppCompatActivity, T : LifecycleOwner { + val focus = existingFocus ?: Focus(0.0f, 0.0f) // Default to center + + val dialogBinding = DialogFocusBinding.inflate(layoutInflater) + + dialogBinding.focusIndicator.setFocus(focus) + + Glide.with(this) + .load(previewUri) + .downsample(DownsampleStrategy.CENTER_INSIDE) + .listener(object : RequestListener<Drawable> { + override fun onLoadFailed( + p0: GlideException?, + p1: Any?, + p2: Target<Drawable?>, + p3: Boolean + ): Boolean { + return false + } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target<Drawable?>?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + val width = resource.intrinsicWidth + val height = resource.intrinsicHeight + + dialogBinding.focusIndicator.setImageSize(width, height) + + // We want the dialog to be a little taller than the image, so you can slide your thumb past the image border, + // but if it's *too* much taller that looks weird. See if a threshold has been crossed: + if (width > height) { + val maxHeight = dialogBinding.focusIndicator.maxAttractiveHeight() + + if (dialogBinding.imageView.height > maxHeight) { + val verticalShrinkLayout = FrameLayout.LayoutParams(width, maxHeight) + dialogBinding.imageView.layoutParams = verticalShrinkLayout + dialogBinding.focusIndicator.layoutParams = verticalShrinkLayout + } + } + return false // Pass through + } + }) + .into(dialogBinding.imageView) + + val okListener = { dialog: DialogInterface, _: Int -> + lifecycleScope.launch { + onUpdateFocus(dialogBinding.focusIndicator.getFocus()) + } + dialog.dismiss() + } + + val dialog = AlertDialog.Builder(this) + .setView(dialogBinding.root) + .setPositiveButton(android.R.string.ok, okListener) + .setNegativeButton(android.R.string.cancel, null) + .create() + + val window = dialog.window + window?.setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE + ) + + dialog.show() +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeOptionsView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeOptionsView.kt new file mode 100644 index 0000000..e576bfe --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeOptionsView.kt @@ -0,0 +1,71 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.compose.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.RadioGroup +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Status + +class ComposeOptionsView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : RadioGroup( + context, + attrs +) { + + var listener: ComposeOptionsListener? = null + + init { + inflate(context, R.layout.view_compose_options, this) + + setOnCheckedChangeListener { _, checkedId -> + val visibility = when (checkedId) { + R.id.publicRadioButton -> + Status.Visibility.PUBLIC + R.id.unlistedRadioButton -> + Status.Visibility.UNLISTED + R.id.privateRadioButton -> + Status.Visibility.PRIVATE + R.id.directRadioButton -> + Status.Visibility.DIRECT + else -> + Status.Visibility.PUBLIC + } + listener?.onVisibilityChanged(visibility) + } + } + + fun setStatusVisibility(visibility: Status.Visibility) { + val selectedButton = when (visibility) { + Status.Visibility.PUBLIC -> + R.id.publicRadioButton + Status.Visibility.UNLISTED -> + R.id.unlistedRadioButton + Status.Visibility.PRIVATE -> + R.id.privateRadioButton + Status.Visibility.DIRECT -> + R.id.directRadioButton + else -> + R.id.directRadioButton + } + + check(selectedButton) + } +} + +interface ComposeOptionsListener { + fun onVisibilityChanged(visibility: Status.Visibility) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.kt new file mode 100644 index 0000000..c7f6a50 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ComposeScheduleView.kt @@ -0,0 +1,230 @@ +/* Copyright 2019 kyori19 + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ +package com.keylesspalace.tusky.components.compose.view + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.content.res.AppCompatResources +import androidx.constraintlayout.widget.ConstraintLayout +import com.google.android.material.datepicker.CalendarConstraints +import com.google.android.material.datepicker.DateValidatorPointForward +import com.google.android.material.datepicker.MaterialDatePicker +import com.google.android.material.timepicker.MaterialTimePicker +import com.google.android.material.timepicker.TimeFormat +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ViewComposeScheduleBinding +import java.text.ParseException +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +class ComposeScheduleView +@JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + interface OnTimeSetListener { + fun onTimeSet(time: String?) + } + + private var binding = ViewComposeScheduleBinding.inflate( + (context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater), + this + ) + private var listener: OnTimeSetListener? = null + private var dateFormat = SimpleDateFormat.getDateInstance() + private var timeFormat = SimpleDateFormat.getTimeInstance() + private var iso8601 = SimpleDateFormat( + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", + Locale.getDefault() + ).apply { + timeZone = TimeZone.getTimeZone("UTC") + } + + /** The date/time the user has chosen to schedule the status, in UTC */ + private var scheduleDateTimeUtc: Calendar? = null + + init { + binding.scheduledDateTime.setOnClickListener { openPickDateDialog() } + binding.invalidScheduleWarning.setText(R.string.warning_scheduling_interval) + updateScheduleUi() + setEditIcons() + } + + fun setListener(listener: OnTimeSetListener?) { + this.listener = listener + } + + private fun updateScheduleUi() { + if (scheduleDateTimeUtc == null) { + binding.scheduledDateTime.text = "" + binding.invalidScheduleWarning.visibility = GONE + return + } + + val scheduled = scheduleDateTimeUtc!!.time + binding.scheduledDateTime.text = String.format( + "%s %s", + dateFormat.format(scheduled), + timeFormat.format(scheduled) + ) + verifyScheduledTime(scheduled) + } + + private fun setEditIcons() { + val icon = AppCompatResources.getDrawable(context, R.drawable.ic_create_24dp) ?: return + val size = binding.scheduledDateTime.lineHeight + icon.setBounds(0, 0, size, size) + binding.scheduledDateTime.setCompoundDrawablesRelative(null, null, icon, null) + } + + fun setResetOnClickListener(listener: OnClickListener?) { + binding.resetScheduleButton.setOnClickListener(listener) + } + + fun resetSchedule() { + scheduleDateTimeUtc = null + updateScheduleUi() + } + + fun openPickDateDialog() { + // The earliest point in time the calendar should display. Start with current date/time + val earliest = calendar().apply { + // Add the minimum scheduling interval. This may roll the calendar over to the + // next day (e.g. if the current time is 23:57). + add(Calendar.SECOND, MINIMUM_SCHEDULED_SECONDS) + // Clear out the time components, so it's midnight + set(Calendar.HOUR_OF_DAY, 0) + set(Calendar.MINUTE, 0) + set(Calendar.SECOND, 0) + set(Calendar.MILLISECOND, 0) + } + val calendarConstraints = CalendarConstraints.Builder() + .setValidator(DateValidatorPointForward.from(earliest.timeInMillis)) + .build() + initializeSuggestedTime() + + // Work around a misfeature in MaterialDatePicker. The `selection` is treated as + // millis-from-epoch, in UTC, which is good. However, it is also *displayed* in UTC + // instead of converting to the user's local timezone. + // + // So we have to add the TZ offset before setting it in the picker + val tzOffset = TimeZone.getDefault().getOffset(scheduleDateTimeUtc!!.timeInMillis) + + val picker = MaterialDatePicker.Builder + .datePicker() + .setSelection(scheduleDateTimeUtc!!.timeInMillis + tzOffset) + .setCalendarConstraints(calendarConstraints) + .build() + picker.addOnPositiveButtonClickListener { selection: Long -> onDateSet(selection) } + picker.show((context as AppCompatActivity).supportFragmentManager, "date_picker") + } + + private fun getTimeFormat(context: Context): Int { + return if (android.text.format.DateFormat.is24HourFormat(context)) { + TimeFormat.CLOCK_24H + } else { + TimeFormat.CLOCK_12H + } + } + + private fun openPickTimeDialog() { + val pickerBuilder = MaterialTimePicker.Builder() + scheduleDateTimeUtc?.let { + pickerBuilder.setHour(it[Calendar.HOUR_OF_DAY]) + .setMinute(it[Calendar.MINUTE]) + } + + pickerBuilder.setTitleText(dateFormat.format(scheduleDateTimeUtc!!.timeInMillis)) + pickerBuilder.setTimeFormat(getTimeFormat(context)) + + val picker = pickerBuilder.build() + picker.addOnPositiveButtonClickListener { onTimeSet(picker.hour, picker.minute) } + picker.show((context as AppCompatActivity).supportFragmentManager, "time_picker") + } + + fun getDateTime(scheduledAt: String?): Date? { + scheduledAt?.let { + try { + return iso8601.parse(it) + } catch (_: ParseException) { + } + } + return null + } + + fun setDateTime(scheduledAt: String?) { + val date = getDateTime(scheduledAt) ?: return + initializeSuggestedTime() + scheduleDateTimeUtc!!.time = date + updateScheduleUi() + } + + fun verifyScheduledTime(scheduledTime: Date?): Boolean { + val valid: Boolean = if (scheduledTime != null) { + val minimumScheduledTime = calendar() + minimumScheduledTime.add( + Calendar.SECOND, + MINIMUM_SCHEDULED_SECONDS + ) + scheduledTime.after(minimumScheduledTime.time) + } else { + true + } + binding.invalidScheduleWarning.visibility = if (valid) GONE else VISIBLE + return valid + } + + private fun onDateSet(selection: Long) { + initializeSuggestedTime() + val newDate = calendar() + // working around bug in DatePicker where date is UTC #1720 + // see https://github.com/material-components/material-components-android/issues/882 + newDate.timeZone = TimeZone.getTimeZone("UTC") + newDate.timeInMillis = selection + scheduleDateTimeUtc!![newDate[Calendar.YEAR], newDate[Calendar.MONTH]] = newDate[Calendar.DATE] + openPickTimeDialog() + } + + private fun onTimeSet(hourOfDay: Int, minute: Int) { + initializeSuggestedTime() + scheduleDateTimeUtc?.set(Calendar.HOUR_OF_DAY, hourOfDay) + scheduleDateTimeUtc?.set(Calendar.MINUTE, minute) + updateScheduleUi() + listener?.onTimeSet(time) + } + + val time: String? + get() = scheduleDateTimeUtc?.time?.let { iso8601.format(it) } + + private fun initializeSuggestedTime() { + if (scheduleDateTimeUtc == null) { + scheduleDateTimeUtc = calendar().apply { + add(Calendar.MINUTE, 15) + } + } + } + + companion object { + // Minimum is 5 minutes, pad 30 seconds for posting + private const val MINIMUM_SCHEDULED_SECONDS = 330 + fun calendar(): Calendar = Calendar.getInstance(TimeZone.getDefault()) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt new file mode 100644 index 0000000..087a703 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/EditTextTyped.kt @@ -0,0 +1,78 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.compose.view + +import android.content.Context +import android.text.InputType +import android.text.method.KeyListener +import android.util.AttributeSet +import android.view.inputmethod.EditorInfo +import android.view.inputmethod.InputConnection +import androidx.appcompat.widget.AppCompatMultiAutoCompleteTextView +import androidx.core.view.OnReceiveContentListener +import androidx.core.view.ViewCompat +import androidx.core.view.inputmethod.EditorInfoCompat +import androidx.core.view.inputmethod.InputConnectionCompat +import androidx.emoji2.viewsintegration.EmojiEditTextHelper + +class EditTextTyped @JvmOverloads constructor( + context: Context, + attributeSet: AttributeSet? = null +) : + AppCompatMultiAutoCompleteTextView(context, attributeSet) { + + private val emojiEditTextHelper: EmojiEditTextHelper = EmojiEditTextHelper(this) + + init { + // fix a bug with autocomplete and some keyboards + val newInputType = inputType and (inputType xor InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE) + inputType = newInputType + super.setKeyListener(emojiEditTextHelper.getKeyListener(keyListener)) + } + + override fun setKeyListener(input: KeyListener?) { + if (input != null) { + super.setKeyListener(emojiEditTextHelper.getKeyListener(input)) + } else { + super.setKeyListener(input) + } + } + + fun setOnReceiveContentListener(listener: OnReceiveContentListener) { + ViewCompat.setOnReceiveContentListener(this, arrayOf("image/*"), listener) + } + + override fun onCreateInputConnection(editorInfo: EditorInfo): InputConnection { + val connection = super.onCreateInputConnection(editorInfo) + EditorInfoCompat.setContentMimeTypes(editorInfo, arrayOf("image/*")) + return emojiEditTextHelper.onCreateInputConnection( + InputConnectionCompat.createWrapper(this, connection, editorInfo), + editorInfo + )!! + } + + /** + * Override pasting to ensure that formatted content is always pasted as + * plain text. + */ + override fun onTextContextMenuItem(id: Int): Boolean { + if (id == android.R.id.paste) { + return super.onTextContextMenuItem(android.R.id.pasteAsPlainText) + } + + return super.onTextContextMenuItem(id) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt new file mode 100644 index 0000000..6c3d75c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/FocusIndicatorView.kt @@ -0,0 +1,140 @@ +package com.keylesspalace.tusky.components.compose.view + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import android.graphics.Point +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import com.keylesspalace.tusky.entity.Attachment +import kotlin.math.ceil +import kotlin.math.max +import kotlin.math.min + +class FocusIndicatorView +@JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + private var focus: Attachment.Focus? = null + private var imageSize: Point? = null + private var circleRadius: Float? = null + + fun setImageSize(width: Int, height: Int) { + this.imageSize = Point(width, height) + if (focus != null) { + invalidate() + } + } + + fun setFocus(focus: Attachment.Focus) { + this.focus = focus + if (imageSize != null) { + invalidate() + } + } + + // Assumes setFocus called first + fun getFocus(): Attachment.Focus { + return focus!! + } + + // This needs to be consistent every time it is consulted over the lifetime of the object, + // so base it on the view width/height whenever the first access occurs. + private fun getCircleRadius(): Float { + val circleRadius = this.circleRadius + if (circleRadius != null) { + return circleRadius + } + val newCircleRadius = min(this.width, this.height).toFloat() / 4.0f + this.circleRadius = newCircleRadius + return newCircleRadius + } + + // Remember focus uses -1..1 y-down coordinates (so focus value should be negated for y) + private fun axisToFocus(value: Float, innerLimit: Int, outerLimit: Int): Float { + val offset = (outerLimit - innerLimit) / 2 // Assume image is centered in widget frame + val result = (value - offset) / innerLimit.toFloat() * 2.0f - 1.0f // To range -1..1 + return min(1.0f, max(-1.0f, result)) // Clamp + } + + private fun axisFromFocus(value: Float, innerLimit: Int, outerLimit: Int): Float { + val offset = (outerLimit - innerLimit) / 2 + return offset.toFloat() + ((value + 1.0f) / 2.0f) * innerLimit.toFloat() // From range -1..1 + } + + @SuppressLint( + "ClickableViewAccessibility" + ) // Android Studio wants us to implement PerformClick for accessibility, but that unfortunately cannot be made meaningful for this widget. + override fun onTouchEvent(event: MotionEvent): Boolean { + if (event.actionMasked == MotionEvent.ACTION_CANCEL) { + return false + } + + val imageSize = this.imageSize ?: return false + + // Convert touch xy to point inside image + focus = Attachment.Focus(axisToFocus(event.x, imageSize.x, this.width), -axisToFocus(event.y, imageSize.y, this.height)) + invalidate() + return true + } + + private val transparentDarkGray = 0x40000000 + private val strokeWidth = 4.0f * this.resources.displayMetrics.density + + private val curtainPaint = Paint(Paint.ANTI_ALIAS_FLAG) + private val strokePaint = Paint(Paint.ANTI_ALIAS_FLAG) + + private val curtainPath = Path() + + init { + curtainPaint.color = transparentDarkGray + curtainPaint.style = Paint.Style.FILL + + strokePaint.style = Paint.Style.STROKE + strokePaint.strokeWidth = strokeWidth + strokePaint.color = Color.WHITE + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + val imageSize = this.imageSize + val focus = this.focus + + if (imageSize != null && focus?.x != null && focus.y != null) { + val x = axisFromFocus(focus.x, imageSize.x, this.width) + val y = axisFromFocus(-focus.y, imageSize.y, this.height) + val circleRadius = getCircleRadius() + + curtainPath.reset() // Draw a flood fill with a hole cut out of it + curtainPath.fillType = Path.FillType.WINDING + curtainPath.addRect( + 0.0f, + 0.0f, + this.width.toFloat(), + this.height.toFloat(), + Path.Direction.CW + ) + curtainPath.addCircle(x, y, circleRadius, Path.Direction.CCW) + canvas.drawPath(curtainPath, curtainPaint) + + canvas.drawCircle(x, y, circleRadius, strokePaint) // Draw white circle + canvas.drawCircle(x, y, strokeWidth / 2.0f, strokePaint) // Draw white dot + } + } + + // Give a "safe" height based on currently set image size. Assume imageSize is set and height>width already checked + fun maxAttractiveHeight(): Int { + val height = this.imageSize!!.y + val circleRadius = getCircleRadius() + + // Give us enough space for the image, plus on each side half a focus indicator circle, plus a strokeWidth + return ceil(height.toFloat() + circleRadius * 2.0f + strokeWidth).toInt() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt new file mode 100644 index 0000000..c55e8fc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/PollPreviewView.kt @@ -0,0 +1,63 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.compose.view + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.PreviewPollOptionsAdapter +import com.keylesspalace.tusky.databinding.ViewPollPreviewBinding +import com.keylesspalace.tusky.entity.NewPoll + +class PollPreviewView @JvmOverloads constructor( + context: Context?, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : + LinearLayout(context, attrs, defStyleAttr) { + + private val adapter = PreviewPollOptionsAdapter() + + private val binding = ViewPollPreviewBinding.inflate(LayoutInflater.from(context), this) + + init { + orientation = VERTICAL + + setBackgroundResource(R.drawable.card_frame) + + val padding = resources.getDimensionPixelSize(R.dimen.poll_preview_padding) + + setPadding(padding, padding, padding, padding) + + binding.pollPreviewOptions.adapter = adapter + } + + fun setPoll(poll: NewPoll) { + adapter.update(poll.options, poll.multiple) + + val pollDurationId = resources.getIntArray(R.array.poll_duration_values).indexOfLast { + it <= poll.expiresIn + } + binding.pollDurationPreview.text = resources.getStringArray(R.array.poll_duration_names)[pollDurationId] + } + + override fun setOnClickListener(l: OnClickListener?) { + super.setOnClickListener(l) + adapter.setOnClickListener(l) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.kt new file mode 100644 index 0000000..8bdaa46 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/ProgressImageView.kt @@ -0,0 +1,109 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ +package com.keylesspalace.tusky.components.compose.view + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.PorterDuff +import android.graphics.PorterDuffXfermode +import android.graphics.RectF +import android.util.AttributeSet +import androidx.appcompat.content.res.AppCompatResources +import at.connyduck.sparkbutton.helpers.Utils +import com.google.android.material.R as materialR +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.view.MediaPreviewImageView + +class ProgressImageView +@JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MediaPreviewImageView(context, attrs, defStyleAttr) { + private var progress = -1 + private val progressRect = RectF() + private val biggerRect = RectF() + private val circlePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = MaterialColors.getColor(this@ProgressImageView, materialR.attr.colorPrimary) + strokeWidth = Utils.dpToPx(context, 4).toFloat() + style = Paint.Style.STROKE + } + private val clearPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + xfermode = PorterDuffXfermode(PorterDuff.Mode.DST_OUT) + } + private val markBgPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + color = MaterialColors.getColor(this@ProgressImageView, android.R.attr.colorBackground) + } + private val captionDrawable = AppCompatResources.getDrawable( + context, + R.drawable.spellcheck + )!!.apply { + setTint( + MaterialColors.getColor(this@ProgressImageView, android.R.attr.textColorTertiary) + ) + } + private val circleRadius = Utils.dpToPx(context, 14) + private val circleMargin = Utils.dpToPx(context, 14) + + fun setProgress(progress: Int) { + this.progress = progress + if (progress != -1) { + setColorFilter(Color.rgb(123, 123, 123), PorterDuff.Mode.MULTIPLY) + } else { + clearColorFilter() + } + invalidate() + } + + fun setChecked(checked: Boolean) { + val backgroundColor = if (checked) materialR.attr.colorPrimary else android.R.attr.colorBackground + val foregroundColor = if (checked) materialR.attr.colorOnPrimary else android.R.attr.textColorTertiary + markBgPaint.color = MaterialColors.getColor(this, backgroundColor) + captionDrawable.setTint(MaterialColors.getColor(this, foregroundColor)) + invalidate() + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + val angle = progress / 100f * 360 - 90 + val halfWidth = width / 2f + val halfHeight = height / 2f + progressRect[halfWidth * 0.75f, halfHeight * 0.75f, halfWidth * 1.25f] = halfHeight * 1.25f + biggerRect.set(progressRect) + val margin = 8 + biggerRect[progressRect.left - margin, progressRect.top - margin, progressRect.right + margin] = + progressRect.bottom + margin + canvas.saveLayer(biggerRect, null) + if (progress != -1) { + canvas.drawOval(progressRect, circlePaint) + canvas.drawArc(biggerRect, angle, 360 - angle - 90, true, clearPaint) + } + canvas.restore() + val circleY = height - circleMargin - circleRadius / 2 + val circleX = width - circleMargin - circleRadius / 2 + canvas.drawCircle(circleX.toFloat(), circleY.toFloat(), circleRadius.toFloat(), markBgPaint) + captionDrawable.setBounds( + width - circleMargin - circleRadius, + height - circleMargin - circleRadius, + width - circleMargin, + height - circleMargin + ) + captionDrawable.draw(canvas) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/view/TootButton.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/TootButton.kt new file mode 100644 index 0000000..995e400 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/view/TootButton.kt @@ -0,0 +1,74 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.compose.view + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import com.google.android.material.button.MaterialButton +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Status +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp + +class TootButton +@JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MaterialButton(context, attrs, defStyleAttr) { + + private val smallStyle: Boolean = context.resources.getBoolean(R.bool.show_small_toot_button) + + init { + if (smallStyle) { + setIconResource(R.drawable.ic_send_24dp) + } else { + setText(R.string.action_send) + iconGravity = ICON_GRAVITY_TEXT_START + } + val padding = resources.getDimensionPixelSize(R.dimen.toot_button_horizontal_padding) + setPadding(padding, 0, padding, 0) + } + + fun setStatusVisibility(visibility: Status.Visibility) { + if (!smallStyle) { + icon = when (visibility) { + Status.Visibility.PUBLIC -> { + setText(R.string.action_send_public) + null + } + Status.Visibility.UNLISTED -> { + setText(R.string.action_send) + null + } + Status.Visibility.PRIVATE, + Status.Visibility.DIRECT -> { + setText(R.string.action_send) + IconicsDrawable(context, GoogleMaterial.Icon.gmd_lock).apply { + sizeDp = 18 + colorInt = Color.WHITE + } + } + else -> { + null + } + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt new file mode 100644 index 0000000..beca734 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationAdapter.kt @@ -0,0 +1,91 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.conversation + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.StatusDisplayOptions + +class ConversationAdapter( + private var statusDisplayOptions: StatusDisplayOptions, + private val listener: StatusActionListener +) : PagingDataAdapter<ConversationViewData, ConversationViewHolder>(CONVERSATION_COMPARATOR) { + + var mediaPreviewEnabled: Boolean + get() = statusDisplayOptions.mediaPreviewEnabled + set(mediaPreviewEnabled) { + statusDisplayOptions = statusDisplayOptions.copy( + mediaPreviewEnabled = mediaPreviewEnabled + ) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ConversationViewHolder { + val view = LayoutInflater.from( + parent.context + ).inflate(R.layout.item_conversation, parent, false) + return ConversationViewHolder(view, statusDisplayOptions, listener) + } + + override fun onBindViewHolder(holder: ConversationViewHolder, position: Int) { + onBindViewHolder(holder, position, emptyList()) + } + + override fun onBindViewHolder( + holder: ConversationViewHolder, + position: Int, + payloads: List<Any> + ) { + getItem(position)?.let { conversationViewData -> + holder.setupWithConversation(conversationViewData, payloads.firstOrNull()) + } + } + + companion object { + val CONVERSATION_COMPARATOR = object : DiffUtil.ItemCallback<ConversationViewData>() { + override fun areItemsTheSame( + oldItem: ConversationViewData, + newItem: ConversationViewData + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: ConversationViewData, + newItem: ConversationViewData + ): Boolean { + return false // Items are different always. It allows to refresh timestamp on every view holder update + } + + override fun getChangePayload( + oldItem: ConversationViewData, + newItem: ConversationViewData + ): Any? { + return if (oldItem == newItem) { + // If items are equal - update timestamp only + listOf(StatusBaseViewHolder.Key.KEY_CREATED) + } else { + // If items are different - update the whole view holder + null + } + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt new file mode 100644 index 0000000..e3c0d5c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationEntity.kt @@ -0,0 +1,199 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.conversation + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.TypeConverters +import com.keylesspalace.tusky.db.Converters +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Conversation +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.HashTag +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.squareup.moshi.JsonClass +import java.util.Date + +@Entity(primaryKeys = ["id", "accountId"]) +@TypeConverters(Converters::class) +data class ConversationEntity( + val accountId: Long, + val id: String, + val order: Int, + val accounts: List<ConversationAccountEntity>, + val unread: Boolean, + @Embedded(prefix = "s_") val lastStatus: ConversationStatusEntity +) { + fun toViewData(): ConversationViewData { + return ConversationViewData( + id = id, + order = order, + accounts = accounts, + unread = unread, + lastStatus = lastStatus.toViewData() + ) + } +} + +@JsonClass(generateAdapter = true) +data class ConversationAccountEntity( + val id: String, + val localUsername: String, + val username: String, + val displayName: String, + val avatar: String, + val emojis: List<Emoji> +) { + fun toAccount(): TimelineAccount { + return TimelineAccount( + id = id, + localUsername = localUsername, + username = username, + displayName = displayName, + note = "", + url = "", + avatar = avatar, + emojis = emojis + ) + } +} + +@TypeConverters(Converters::class) +data class ConversationStatusEntity( + val id: String, + val url: String?, + val inReplyToId: String?, + val inReplyToAccountId: String?, + val account: ConversationAccountEntity, + val content: String, + val createdAt: Date, + val editedAt: Date?, + val emojis: List<Emoji>, + val favouritesCount: Int, + val repliesCount: Int, + val favourited: Boolean, + val bookmarked: Boolean, + val sensitive: Boolean, + val spoilerText: String, + val attachments: List<Attachment>, + val mentions: List<Status.Mention>, + val tags: List<HashTag>?, + val showingHiddenContent: Boolean, + val expanded: Boolean, + val collapsed: Boolean, + val muted: Boolean, + val poll: Poll?, + val language: String? +) { + + fun toViewData(): StatusViewData.Concrete { + return StatusViewData.Concrete( + status = Status( + id = id, + url = url, + account = account.toAccount(), + inReplyToId = inReplyToId, + inReplyToAccountId = inReplyToAccountId, + content = content, + reblog = null, + createdAt = createdAt, + editedAt = editedAt, + emojis = emojis, + reblogsCount = 0, + favouritesCount = favouritesCount, + repliesCount = repliesCount, + reblogged = false, + favourited = favourited, + bookmarked = bookmarked, + sensitive = sensitive, + spoilerText = spoilerText, + visibility = Status.Visibility.DIRECT, + attachments = attachments, + mentions = mentions, + tags = tags.orEmpty(), + application = null, + pinned = false, + muted = muted, + poll = poll, + card = null, + language = language, + filtered = emptyList() + ), + isExpanded = expanded, + isShowingContent = showingHiddenContent, + isCollapsed = collapsed + ) + } +} + +fun TimelineAccount.toEntity() = ConversationAccountEntity( + id = id, + localUsername = localUsername, + username = username, + displayName = name, + avatar = avatar, + emojis = emojis +) + +fun Status.toEntity(expanded: Boolean, contentShowing: Boolean, contentCollapsed: Boolean) = + ConversationStatusEntity( + id = id, + url = url, + inReplyToId = inReplyToId, + inReplyToAccountId = inReplyToAccountId, + account = account.toEntity(), + content = content, + createdAt = createdAt, + editedAt = editedAt, + emojis = emojis, + favouritesCount = favouritesCount, + repliesCount = repliesCount, + favourited = favourited, + bookmarked = bookmarked, + sensitive = sensitive, + spoilerText = spoilerText, + attachments = attachments, + mentions = mentions, + tags = tags, + showingHiddenContent = contentShowing, + expanded = expanded, + collapsed = contentCollapsed, + muted = muted, + poll = poll, + language = language + ) + +fun Conversation.toEntity( + accountId: Long, + order: Int, + expanded: Boolean, + contentShowing: Boolean, + contentCollapsed: Boolean +) = ConversationEntity( + accountId = accountId, + id = id, + order = order, + accounts = accounts.map { it.toEntity() }, + unread = unread, + lastStatus = lastStatus!!.toEntity( + expanded = expanded, + contentShowing = contentShowing, + contentCollapsed = contentCollapsed + ) +) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt new file mode 100644 index 0000000..b8373f9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationLoadStateAdapter.kt @@ -0,0 +1,60 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.conversation + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.LoadState +import androidx.paging.LoadStateAdapter +import com.keylesspalace.tusky.databinding.ItemNetworkStateBinding +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.visible + +class ConversationLoadStateAdapter( + private val retryCallback: () -> Unit +) : LoadStateAdapter<BindingHolder<ItemNetworkStateBinding>>() { + + override fun onBindViewHolder( + holder: BindingHolder<ItemNetworkStateBinding>, + loadState: LoadState + ) { + val binding = holder.binding + binding.progressBar.visible(loadState == LoadState.Loading) + binding.retryButton.visible(loadState is LoadState.Error) + val msg = if (loadState is LoadState.Error) { + loadState.error.message + } else { + null + } + binding.errorMsg.visible(msg != null) + binding.errorMsg.text = msg + binding.retryButton.setOnClickListener { + retryCallback() + } + } + + override fun onCreateViewHolder( + parent: ViewGroup, + loadState: LoadState + ): BindingHolder<ItemNetworkStateBinding> { + val binding = ItemNetworkStateBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return BindingHolder(binding) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt new file mode 100644 index 0000000..9444254 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewData.kt @@ -0,0 +1,92 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.conversation + +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.viewdata.StatusViewData + +data class ConversationViewData( + val id: String, + val order: Int, + val accounts: List<ConversationAccountEntity>, + val unread: Boolean, + val lastStatus: StatusViewData.Concrete +) { + fun toEntity( + accountId: Long, + favourited: Boolean = lastStatus.status.favourited, + bookmarked: Boolean = lastStatus.status.bookmarked, + muted: Boolean = lastStatus.status.muted, + poll: Poll? = lastStatus.status.poll, + expanded: Boolean = lastStatus.isExpanded, + collapsed: Boolean = lastStatus.isCollapsed, + showingHiddenContent: Boolean = lastStatus.isShowingContent + ): ConversationEntity { + return ConversationEntity( + accountId = accountId, + id = id, + order = order, + accounts = accounts, + unread = unread, + lastStatus = lastStatus.toConversationStatusEntity( + favourited = favourited, + bookmarked = bookmarked, + muted = muted, + poll = poll, + expanded = expanded, + collapsed = collapsed, + showingHiddenContent = showingHiddenContent + ) + ) + } +} + +fun StatusViewData.Concrete.toConversationStatusEntity( + favourited: Boolean = status.favourited, + bookmarked: Boolean = status.bookmarked, + muted: Boolean = status.muted, + poll: Poll? = status.poll, + expanded: Boolean = isExpanded, + collapsed: Boolean = isCollapsed, + showingHiddenContent: Boolean = isShowingContent +): ConversationStatusEntity { + return ConversationStatusEntity( + id = id, + url = status.url, + inReplyToId = status.inReplyToId, + inReplyToAccountId = status.inReplyToAccountId, + account = status.account.toEntity(), + content = status.content, + createdAt = status.createdAt, + editedAt = status.editedAt, + emojis = status.emojis, + favouritesCount = status.favouritesCount, + repliesCount = status.repliesCount, + favourited = favourited, + bookmarked = bookmarked, + sensitive = status.sensitive, + spoilerText = status.spoilerText, + attachments = status.attachments, + mentions = status.mentions, + tags = status.tags, + showingHiddenContent = showingHiddenContent, + expanded = expanded, + collapsed = collapsed, + muted = muted, + poll = poll, + language = status.language + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java new file mode 100644 index 0000000..f73fe85 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationViewHolder.java @@ -0,0 +1,181 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.conversation; + +import android.content.Context; +import android.text.InputFilter; +import android.text.TextUtils; +import android.view.View; +import android.widget.Button; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder; +import com.keylesspalace.tusky.entity.Attachment; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.entity.TimelineAccount; +import com.keylesspalace.tusky.interfaces.StatusActionListener; +import com.keylesspalace.tusky.util.ImageLoadingHelper; +import com.keylesspalace.tusky.util.SmartLengthInputFilter; +import com.keylesspalace.tusky.util.StatusDisplayOptions; +import com.keylesspalace.tusky.viewdata.StatusViewData; + +import java.util.List; + +public class ConversationViewHolder extends StatusBaseViewHolder { + private static final InputFilter[] COLLAPSE_INPUT_FILTER = new InputFilter[]{SmartLengthInputFilter.INSTANCE}; + private static final InputFilter[] NO_INPUT_FILTER = new InputFilter[0]; + + private final TextView conversationNameTextView; + private final Button contentCollapseButton; + private final ImageView[] avatars; + + private final StatusDisplayOptions statusDisplayOptions; + private final StatusActionListener listener; + + ConversationViewHolder(View itemView, + StatusDisplayOptions statusDisplayOptions, + StatusActionListener listener) { + super(itemView); + conversationNameTextView = itemView.findViewById(R.id.conversation_name); + contentCollapseButton = itemView.findViewById(R.id.button_toggle_content); + avatars = new ImageView[]{ + avatar, + itemView.findViewById(R.id.status_avatar_1), + itemView.findViewById(R.id.status_avatar_2) + }; + this.statusDisplayOptions = statusDisplayOptions; + + this.listener = listener; + } + + void setupWithConversation( + @NonNull ConversationViewData conversation, + @Nullable Object payloads + ) { + + StatusViewData.Concrete statusViewData = conversation.getLastStatus(); + Status status = statusViewData.getStatus(); + + if (payloads == null) { + TimelineAccount account = status.getAccount(); + + setupCollapsedState(statusViewData.isCollapsible(), statusViewData.isCollapsed(), statusViewData.isExpanded(), status.getSpoilerText(), listener); + + String displayName = account.getDisplayName(); + if (displayName == null) { + displayName = ""; + } + setDisplayName(displayName, account.getEmojis(), statusDisplayOptions); + setUsername(account.getUsername()); + setMetaData(statusViewData, statusDisplayOptions, listener); + setIsReply(status.getInReplyToId() != null); + setFavourited(status.getFavourited()); + setBookmarked(status.getBookmarked()); + List<Attachment> attachments = status.getAttachments(); + boolean sensitive = status.getSensitive(); + if (statusDisplayOptions.mediaPreviewEnabled() && hasPreviewableAttachment(attachments)) { + setMediaPreviews(attachments, sensitive, listener, statusViewData.isShowingContent(), + statusDisplayOptions.useBlurhash()); + + if (attachments.isEmpty()) { + hideSensitiveMediaWarning(); + } + // Hide the unused label. + for (TextView mediaLabel : mediaLabels) { + mediaLabel.setVisibility(View.GONE); + } + } else { + setMediaLabel(attachments, sensitive, listener, statusViewData.isShowingContent()); + // Hide all unused views. + mediaPreview.setVisibility(View.GONE); + hideSensitiveMediaWarning(); + } + + setupButtons(listener, account.getId(), statusViewData.getContent().toString(), + statusDisplayOptions); + + setSpoilerAndContent(statusViewData, statusDisplayOptions, listener); + + setConversationName(conversation.getAccounts()); + + setAvatars(conversation.getAccounts()); + } else { + if (payloads instanceof List) { + for (Object item : (List<?>) payloads) { + if (Key.KEY_CREATED.equals(item)) { + setMetaData(statusViewData, statusDisplayOptions, listener); + } + } + } + } + } + + private void setConversationName(List<ConversationAccountEntity> accounts) { + Context context = conversationNameTextView.getContext(); + String conversationName = ""; + if (accounts.size() == 1) { + conversationName = context.getString(R.string.conversation_1_recipients, accounts.get(0).getUsername()); + } else if (accounts.size() == 2) { + conversationName = context.getString(R.string.conversation_2_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername()); + } else if (accounts.size() > 2) { + conversationName = context.getString(R.string.conversation_more_recipients, accounts.get(0).getUsername(), accounts.get(1).getUsername(), accounts.size() - 2); + } + + conversationNameTextView.setText(conversationName); + } + + private void setAvatars(List<ConversationAccountEntity> accounts) { + for (int i = 0; i < avatars.length; i++) { + ImageView avatarView = avatars[i]; + if (i < accounts.size()) { + ImageLoadingHelper.loadAvatar(accounts.get(i).getAvatar(), avatarView, + avatarRadius48dp, statusDisplayOptions.animateAvatars(), null); + avatarView.setVisibility(View.VISIBLE); + } else { + avatarView.setVisibility(View.GONE); + } + } + } + + private void setupCollapsedState(boolean collapsible, boolean collapsed, boolean expanded, String spoilerText, final StatusActionListener listener) { + /* input filter for TextViews have to be set before text */ + if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { + contentCollapseButton.setOnClickListener(view -> { + int position = getBindingAdapterPosition(); + if (position != RecyclerView.NO_POSITION) + listener.onContentCollapsedChange(!collapsed, position); + }); + + contentCollapseButton.setVisibility(View.VISIBLE); + if (collapsed) { + contentCollapseButton.setText(R.string.post_content_warning_show_more); + content.setFilters(COLLAPSE_INPUT_FILTER); + } else { + contentCollapseButton.setText(R.string.post_content_warning_show_less); + content.setFilters(NO_INPUT_FILTER); + } + } else { + contentCollapseButton.setVisibility(View.GONE); + content.setFilters(NO_INPUT_FILTER); + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt new file mode 100644 index 0000000..c97cacc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsFragment.kt @@ -0,0 +1,398 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.conversation + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.PopupMenu +import androidx.core.view.MenuProvider +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.paging.LoadState +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator +import at.connyduck.sparkbutton.helpers.Utils +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.StatusListActivity +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.appstore.ConversationsLoadingEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.components.account.AccountActivity +import com.keylesspalace.tusky.databinding.FragmentTimelineBinding +import com.keylesspalace.tusky.fragment.SFragment +import com.keylesspalace.tusky.interfaces.ReselectableFragment +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.isAnyLoading +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlin.time.DurationUnit +import kotlin.time.toDuration +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class ConversationsFragment : + SFragment(R.layout.fragment_timeline), + StatusActionListener, + ReselectableFragment, + MenuProvider { + + @Inject + lateinit var eventHub: EventHub + + @Inject + lateinit var preferences: SharedPreferences + + private val viewModel: ConversationsViewModel by viewModels() + + private val binding by viewBinding(FragmentTimelineBinding::bind) + + private var adapter: ConversationAdapter? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) + + val statusDisplayOptions = StatusDisplayOptions( + animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true, + useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false), + showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true), + useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true), + cardViewMode = CardViewMode.NONE, + confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true), + confirmFavourites = preferences.getBoolean(PrefKeys.CONFIRM_FAVOURITES, false), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), + showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false), + showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia, + openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler + ) + + val adapter = ConversationAdapter(statusDisplayOptions, this) + this.adapter = adapter + + setupRecyclerView(adapter) + + binding.swipeRefreshLayout.setOnRefreshListener { refreshContent() } + + adapter.addLoadStateListener { loadState -> + if (loadState.refresh != LoadState.Loading && loadState.source.refresh != LoadState.Loading) { + binding.swipeRefreshLayout.isRefreshing = false + } + + binding.statusView.hide() + binding.progressBar.hide() + + if (loadState.isAnyLoading()) { + viewLifecycleOwner.lifecycleScope.launch { + eventHub.dispatch( + ConversationsLoadingEvent( + accountManager.activeAccount?.accountId ?: "" + ) + ) + } + } + + if (adapter.itemCount == 0) { + when (loadState.refresh) { + is LoadState.NotLoading -> { + if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) { + binding.statusView.show() + binding.statusView.setup( + R.drawable.elephant_friend_empty, + R.string.message_empty, + null + ) + binding.statusView.showHelp(R.string.help_empty_conversations) + } + } + + is LoadState.Error -> { + binding.statusView.show() + binding.statusView.setup( + (loadState.refresh as LoadState.Error).error + ) { refreshContent() } + } + + is LoadState.Loading -> { + binding.progressBar.show() + } + } + } + } + + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + val firstPos = (binding.recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + if (firstPos == 0 && positionStart == 0 && adapter.itemCount != itemCount) { + binding.recyclerView.post { + if (getView() != null) { + binding.recyclerView.scrollBy(0, Utils.dpToPx(requireContext(), -30)) + } + } + } + } + }) + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.conversationFlow.collectLatest { pagingData -> + adapter.submitData(pagingData) + } + } + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false) + while (!useAbsoluteTime) { + adapter.notifyItemRangeChanged( + 0, + adapter.itemCount, + listOf(StatusBaseViewHolder.Key.KEY_CREATED) + ) + delay(1.toDuration(DurationUnit.MINUTES)) + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + eventHub.events.collect { event -> + if (event is PreferenceChangedEvent) { + onPreferenceChanged(adapter, event.preferenceKey) + } + } + } + } + + override fun onDestroyView() { + // Clear the adapter to prevent leaking the View + adapter = null + super.onDestroyView() + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.fragment_conversations, menu) + menu.findItem(R.id.action_refresh)?.apply { + icon = IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_refresh).apply { + sizeDp = 20 + colorInt = MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary) + } + } + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_refresh -> { + binding.swipeRefreshLayout.isRefreshing = true + refreshContent() + true + } + + else -> false + } + } + + private fun setupRecyclerView(adapter: ConversationAdapter) { + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(context) + + binding.recyclerView.addItemDecoration( + DividerItemDecoration(context, DividerItemDecoration.VERTICAL) + ) + + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + + binding.recyclerView.adapter = + adapter.withLoadStateFooter(ConversationLoadStateAdapter(adapter::retry)) + } + + private fun refreshContent() { + adapter?.refresh() + } + + override fun onReblog(reblog: Boolean, position: Int) { + // its impossible to reblog private messages + } + + override fun onFavourite(favourite: Boolean, position: Int) { + adapter?.peek(position)?.let { conversation -> + viewModel.favourite(favourite, conversation) + } + } + + override fun onBookmark(favourite: Boolean, position: Int) { + adapter?.peek(position)?.let { conversation -> + viewModel.bookmark(favourite, conversation) + } + } + + override val onMoreTranslate: ((translate: Boolean, position: Int) -> Unit)? = null + + override fun onMore(view: View, position: Int) { + adapter?.peek(position)?.let { conversation -> + + val popup = PopupMenu(requireContext(), view) + popup.inflate(R.menu.conversation_more) + + if (conversation.lastStatus.status.muted) { + popup.menu.removeItem(R.id.status_mute_conversation) + } else { + popup.menu.removeItem(R.id.status_unmute_conversation) + } + + popup.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.status_mute_conversation -> viewModel.muteConversation(conversation) + R.id.status_unmute_conversation -> viewModel.muteConversation(conversation) + R.id.conversation_delete -> deleteConversation(conversation) + } + true + } + popup.show() + } + } + + override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { + adapter?.peek(position)?.let { conversation -> + viewMedia( + attachmentIndex, + AttachmentViewData.list(conversation.lastStatus), + view + ) + } + } + + override fun onViewThread(position: Int) { + adapter?.peek(position)?.let { conversation -> + viewThread(conversation.lastStatus.id, conversation.lastStatus.status.url) + } + } + + override fun onOpenReblog(position: Int) { + // there are no reblogs in conversations + } + + override fun onExpandedChange(expanded: Boolean, position: Int) { + adapter?.peek(position)?.let { conversation -> + viewModel.expandHiddenStatus(expanded, conversation) + } + } + + override fun onContentHiddenChange(isShowing: Boolean, position: Int) { + adapter?.peek(position)?.let { conversation -> + viewModel.showContent(isShowing, conversation) + } + } + + override fun onLoadMore(position: Int) { + // not using the old way of pagination + } + + override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { + adapter?.peek(position)?.let { conversation -> + viewModel.collapseLongStatus(isCollapsed, conversation) + } + } + + override fun onViewAccount(id: String) { + val intent = AccountActivity.getIntent(requireContext(), id) + startActivity(intent) + } + + override fun onViewTag(tag: String) { + val intent = StatusListActivity.newHashtagIntent(requireContext(), tag) + startActivity(intent) + } + + override fun removeItem(position: Int) { + // not needed + } + + override fun onReply(position: Int) { + adapter?.peek(position)?.let { conversation -> + reply(conversation.lastStatus.status) + } + } + + override fun onVoteInPoll(position: Int, choices: MutableList<Int>) { + adapter?.peek(position)?.let { conversation -> + viewModel.voteInPoll(choices, conversation) + } + } + + override fun clearWarningAction(position: Int) { + } + + override fun onReselect() { + if (view != null) { + binding.recyclerView.layoutManager?.scrollToPosition(0) + binding.recyclerView.stopScroll() + } + } + + override fun onUntranslate(position: Int) { + // not needed + } + + private fun deleteConversation(conversation: ConversationViewData) { + AlertDialog.Builder(requireContext()) + .setMessage(R.string.dialog_delete_conversation_warning) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok) { _, _ -> + viewModel.remove(conversation) + } + .show() + } + + private fun onPreferenceChanged(adapter: ConversationAdapter, key: String) { + when (key) { + PrefKeys.MEDIA_PREVIEW_ENABLED -> { + val enabled = accountManager.activeAccount!!.mediaPreviewEnabled + val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled + if (enabled != oldMediaPreviewEnabled) { + adapter.mediaPreviewEnabled = enabled + adapter.notifyItemRangeChanged(0, adapter.itemCount) + } + } + } + } + + companion object { + fun newInstance() = ConversationsFragment() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt new file mode 100644 index 0000000..cf81e5e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsRemoteMediator.kt @@ -0,0 +1,84 @@ +package com.keylesspalace.tusky.components.conversation + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import androidx.room.withTransaction +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.HttpHeaderLink +import retrofit2.HttpException + +@OptIn(ExperimentalPagingApi::class) +class ConversationsRemoteMediator( + private val api: MastodonApi, + private val db: AppDatabase, + accountManager: AccountManager +) : RemoteMediator<Int, ConversationEntity>() { + + private var nextKey: String? = null + + private var order: Int = 0 + + private val activeAccount = accountManager.activeAccount!! + + override suspend fun load( + loadType: LoadType, + state: PagingState<Int, ConversationEntity> + ): MediatorResult { + if (loadType == LoadType.PREPEND) { + return MediatorResult.Success(endOfPaginationReached = true) + } + + if (loadType == LoadType.REFRESH) { + nextKey = null + order = 0 + } + + try { + val conversationsResponse = api.getConversations( + maxId = nextKey, + limit = state.config.pageSize + ) + + val conversations = conversationsResponse.body() + if (!conversationsResponse.isSuccessful || conversations == null) { + return MediatorResult.Error(HttpException(conversationsResponse)) + } + + db.withTransaction { + if (loadType == LoadType.REFRESH) { + db.conversationDao().deleteForAccount(activeAccount.id) + } + + val linkHeader = conversationsResponse.headers()["Link"] + val links = HttpHeaderLink.parse(linkHeader) + nextKey = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter("max_id") + + db.conversationDao().insert( + conversations + .filterNot { it.lastStatus == null } + .map { conversation -> + + val expanded = activeAccount.alwaysOpenSpoiler + val contentShowing = activeAccount.alwaysShowSensitiveMedia || !conversation.lastStatus!!.sensitive + val contentCollapsed = true + + conversation.toEntity( + accountId = activeAccount.id, + order = order++, + expanded = expanded, + contentShowing = contentShowing, + contentCollapsed = contentCollapsed + ) + } + ) + } + return MediatorResult.Success(endOfPaginationReached = nextKey == null) + } catch (e: Exception) { + return MediatorResult.Error(e) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt new file mode 100644 index 0000000..27f63ae --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/conversation/ConversationsViewModel.kt @@ -0,0 +1,188 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.conversation + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import androidx.paging.map +import at.connyduck.calladapter.networkresult.fold +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.usecase.TimelineCases +import com.keylesspalace.tusky.util.EmptyPagingSource +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +@HiltViewModel +class ConversationsViewModel @Inject constructor( + private val timelineCases: TimelineCases, + private val database: AppDatabase, + private val accountManager: AccountManager, + private val api: MastodonApi +) : ViewModel() { + + @OptIn(ExperimentalPagingApi::class) + val conversationFlow = Pager( + config = PagingConfig( + pageSize = 30 + ), + remoteMediator = ConversationsRemoteMediator(api, database, accountManager), + pagingSourceFactory = { + val activeAccount = accountManager.activeAccount + if (activeAccount == null) { + EmptyPagingSource() + } else { + database.conversationDao().conversationsForAccount(activeAccount.id) + } + } + ) + .flow + .map { pagingData -> + pagingData.map { conversation -> conversation.toViewData() } + } + .cachedIn(viewModelScope) + + fun favourite(favourite: Boolean, conversation: ConversationViewData) { + viewModelScope.launch { + timelineCases.favourite(conversation.lastStatus.id, favourite).fold({ + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + favourited = favourite + ) + + saveConversationToDb(newConversation) + }, { e -> + Log.w(TAG, "failed to favourite status", e) + }) + } + } + + fun bookmark(bookmark: Boolean, conversation: ConversationViewData) { + viewModelScope.launch { + timelineCases.bookmark(conversation.lastStatus.id, bookmark).fold({ + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + bookmarked = bookmark + ) + + saveConversationToDb(newConversation) + }, { e -> + Log.w(TAG, "failed to bookmark status", e) + }) + } + } + + fun voteInPoll(choices: List<Int>, conversation: ConversationViewData) { + viewModelScope.launch { + timelineCases.voteInPoll( + conversation.lastStatus.id, + conversation.lastStatus.status.poll?.id!!, + choices + ) + .fold({ poll -> + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + poll = poll + ) + + saveConversationToDb(newConversation) + }, { e -> + Log.w(TAG, "failed to vote in poll", e) + }) + } + } + + fun expandHiddenStatus(expanded: Boolean, conversation: ConversationViewData) { + viewModelScope.launch { + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + expanded = expanded + ) + saveConversationToDb(newConversation) + } + } + + fun collapseLongStatus(collapsed: Boolean, conversation: ConversationViewData) { + viewModelScope.launch { + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + collapsed = collapsed + ) + saveConversationToDb(newConversation) + } + } + + fun showContent(showing: Boolean, conversation: ConversationViewData) { + viewModelScope.launch { + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + showingHiddenContent = showing + ) + saveConversationToDb(newConversation) + } + } + + fun remove(conversation: ConversationViewData) { + viewModelScope.launch { + try { + api.deleteConversation(conversationId = conversation.id) + + database.conversationDao().delete( + id = conversation.id, + accountId = accountManager.activeAccount!!.id + ) + } catch (e: Exception) { + Log.w(TAG, "failed to delete conversation", e) + } + } + } + + fun muteConversation(conversation: ConversationViewData) { + viewModelScope.launch { + try { + timelineCases.muteConversation( + conversation.lastStatus.id, + !conversation.lastStatus.status.muted + ) + + val newConversation = conversation.toEntity( + accountId = accountManager.activeAccount!!.id, + muted = !conversation.lastStatus.status.muted + ) + + database.conversationDao().insert(newConversation) + } catch (e: Exception) { + Log.w(TAG, "failed to mute conversation", e) + } + } + } + + private suspend fun saveConversationToDb(conversation: ConversationEntity) { + database.conversationDao().insert(conversation) + } + + companion object { + private const val TAG = "ConversationsViewModel" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksActivity.kt new file mode 100644 index 0000000..3a1ae69 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksActivity.kt @@ -0,0 +1,29 @@ +package com.keylesspalace.tusky.components.domainblocks + +import android.os.Bundle +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ActivityAccountListBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class DomainBlocksActivity : BaseActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val binding = ActivityAccountListBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.apply { + setTitle(R.string.title_domain_mutes) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + supportFragmentManager + .beginTransaction() + .replace(R.id.fragment_container, DomainBlocksFragment()) + .commit() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksAdapter.kt new file mode 100644 index 0000000..29d42aa --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksAdapter.kt @@ -0,0 +1,34 @@ +package com.keylesspalace.tusky.components.domainblocks + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import com.keylesspalace.tusky.components.followedtags.FollowedTagsAdapter.Companion.STRING_COMPARATOR +import com.keylesspalace.tusky.databinding.ItemBlockedDomainBinding +import com.keylesspalace.tusky.util.BindingHolder + +class DomainBlocksAdapter( + private val onUnmute: (String) -> Unit +) : PagingDataAdapter<String, BindingHolder<ItemBlockedDomainBinding>>(STRING_COMPARATOR) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemBlockedDomainBinding> { + val binding = ItemBlockedDomainBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return BindingHolder(binding) + } + + override fun onBindViewHolder(holder: BindingHolder<ItemBlockedDomainBinding>, position: Int) { + getItem(position)?.let { instance -> + holder.binding.blockedDomain.text = instance + holder.binding.blockedDomainUnblock.setOnClickListener { + onUnmute(instance) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksFragment.kt new file mode 100644 index 0000000..1043808 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksFragment.kt @@ -0,0 +1,92 @@ +package com.keylesspalace.tusky.components.domainblocks + +import android.os.Bundle +import android.util.Log +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.FragmentDomainBlocksBinding +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class DomainBlocksFragment : Fragment(R.layout.fragment_domain_blocks) { + + private val binding by viewBinding(FragmentDomainBlocksBinding::bind) + + private val viewModel: DomainBlocksViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val adapter = DomainBlocksAdapter(viewModel::unblock) + + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.addItemDecoration( + DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL) + ) + binding.recyclerView.adapter = adapter + binding.recyclerView.layoutManager = LinearLayoutManager(view.context) + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.uiEvents.collect { event -> + showSnackbar(event) + } + } + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.domainPager.collectLatest { pagingData -> + adapter.submitData(pagingData) + } + } + + adapter.addLoadStateListener { loadState -> + binding.progressBar.visible( + loadState.refresh == LoadState.Loading && adapter.itemCount == 0 + ) + + if (loadState.refresh is LoadState.Error) { + binding.recyclerView.hide() + binding.messageView.show() + val errorState = loadState.refresh as LoadState.Error + binding.messageView.setup(errorState.error) { adapter.retry() } + Log.w(TAG, "error loading blocked domains", errorState.error) + } else if (loadState.refresh is LoadState.NotLoading && adapter.itemCount == 0) { + binding.recyclerView.hide() + binding.messageView.show() + binding.messageView.setup(R.drawable.elephant_friend_empty, R.string.message_empty) + } else { + binding.recyclerView.show() + binding.messageView.hide() + } + } + } + + private fun showSnackbar(event: SnackbarEvent) { + val message = if (event.throwable == null) { + getString(event.message, event.domain) + } else { + Log.w(TAG, event.throwable) + val error = event.throwable.localizedMessage ?: getString(R.string.ui_error_unknown) + getString(event.message, event.domain, error) + } + + Snackbar.make(binding.recyclerView, message, Snackbar.LENGTH_LONG) + .setTextMaxLines(5) + .setAction(event.actionText, event.action) + .show() + } + + companion object { + private const val TAG = "DomainBlocksFragment" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksPagingSource.kt new file mode 100644 index 0000000..0438a26 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksPagingSource.kt @@ -0,0 +1,19 @@ +package com.keylesspalace.tusky.components.domainblocks + +import androidx.paging.PagingSource +import androidx.paging.PagingState + +class DomainBlocksPagingSource( + private val domains: List<String>, + private val nextKey: String? +) : PagingSource<String, String>() { + override fun getRefreshKey(state: PagingState<String, String>): String? = null + + override suspend fun load(params: LoadParams<String>): LoadResult<String, String> { + return if (params is LoadParams.Refresh) { + LoadResult.Page(domains, null, nextKey) + } else { + LoadResult.Page(emptyList(), null, null) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksRemoteMediator.kt new file mode 100644 index 0000000..09f9904 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksRemoteMediator.kt @@ -0,0 +1,57 @@ +package com.keylesspalace.tusky.components.domainblocks + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.HttpHeaderLink +import retrofit2.HttpException +import retrofit2.Response + +@OptIn(ExperimentalPagingApi::class) +class DomainBlocksRemoteMediator( + private val api: MastodonApi, + private val repository: DomainBlocksRepository +) : RemoteMediator<String, String>() { + + override suspend fun load( + loadType: LoadType, + state: PagingState<String, String> + ): MediatorResult { + return try { + val response = request(loadType) + ?: return MediatorResult.Success(endOfPaginationReached = true) + + return applyResponse(response) + } catch (e: Exception) { + MediatorResult.Error(e) + } + } + + private suspend fun request(loadType: LoadType): Response<List<String>>? { + return when (loadType) { + LoadType.PREPEND -> null + LoadType.APPEND -> api.domainBlocks(maxId = repository.nextKey) + LoadType.REFRESH -> { + repository.nextKey = null + repository.domains.clear() + api.domainBlocks() + } + } + } + + private fun applyResponse(response: Response<List<String>>): MediatorResult { + val tags = response.body() + if (!response.isSuccessful || tags == null) { + return MediatorResult.Error(HttpException(response)) + } + + val links = HttpHeaderLink.parse(response.headers()["Link"]) + repository.nextKey = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter("max_id") + repository.domains.addAll(tags) + repository.invalidate() + + return MediatorResult.Success(endOfPaginationReached = repository.nextKey == null) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksRepository.kt new file mode 100644 index 0000000..c1c993d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksRepository.kt @@ -0,0 +1,72 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.components.domainblocks + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.InvalidatingPagingSourceFactory +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingSource +import at.connyduck.calladapter.networkresult.NetworkResult +import at.connyduck.calladapter.networkresult.onSuccess +import com.keylesspalace.tusky.network.MastodonApi +import javax.inject.Inject + +class DomainBlocksRepository @Inject constructor( + private val api: MastodonApi +) { + val domains: MutableList<String> = mutableListOf() + var nextKey: String? = null + + private var factory = InvalidatingPagingSourceFactory { + DomainBlocksPagingSource(domains.toList(), nextKey) + } + + @OptIn(ExperimentalPagingApi::class) + val domainPager = Pager( + config = PagingConfig( + pageSize = PAGE_SIZE, + initialLoadSize = PAGE_SIZE + ), + remoteMediator = DomainBlocksRemoteMediator(api, this), + pagingSourceFactory = factory + ).flow + + /** Invalidate the active paging source, see [PagingSource.invalidate] */ + fun invalidate() { + factory.invalidate() + } + + suspend fun block(domain: String): NetworkResult<Unit> { + return api.blockDomain(domain).onSuccess { + domains.add(domain) + factory.invalidate() + } + } + + suspend fun unblock(domain: String): NetworkResult<Unit> { + return api.unblockDomain(domain).onSuccess { + domains.remove(domain) + factory.invalidate() + } + } + + companion object { + private const val PAGE_SIZE = 20 + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksViewModel.kt new file mode 100644 index 0000000..a07cd84 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/domainblocks/DomainBlocksViewModel.kt @@ -0,0 +1,77 @@ +package com.keylesspalace.tusky.components.domainblocks + +import android.view.View +import androidx.annotation.StringRes +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.cachedIn +import at.connyduck.calladapter.networkresult.fold +import at.connyduck.calladapter.networkresult.onFailure +import com.keylesspalace.tusky.R +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch + +@HiltViewModel +class DomainBlocksViewModel @Inject constructor( + private val repo: DomainBlocksRepository +) : ViewModel() { + + val domainPager = repo.domainPager.cachedIn(viewModelScope) + + private val _uiEvents = MutableSharedFlow<SnackbarEvent>() + val uiEvents: SharedFlow<SnackbarEvent> = _uiEvents.asSharedFlow() + + fun block(domain: String) { + viewModelScope.launch { + repo.block(domain).onFailure { e -> + _uiEvents.emit( + SnackbarEvent( + message = R.string.error_blocking_domain, + domain = domain, + throwable = e, + actionText = R.string.action_retry, + action = { block(domain) } + ) + ) + } + } + } + + fun unblock(domain: String) { + viewModelScope.launch { + repo.unblock(domain).fold({ + _uiEvents.emit( + SnackbarEvent( + message = R.string.confirmation_domain_unmuted, + domain = domain, + throwable = null, + actionText = R.string.action_undo, + action = { block(domain) } + ) + ) + }, { e -> + _uiEvents.emit( + SnackbarEvent( + message = R.string.error_unblocking_domain, + domain = domain, + throwable = e, + actionText = R.string.action_retry, + action = { unblock(domain) } + ) + ) + }) + } + } +} + +class SnackbarEvent( + @StringRes val message: Int, + val domain: String, + val throwable: Throwable?, + @StringRes val actionText: Int, + val action: (View) -> Unit +) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt new file mode 100644 index 0000000..e688960 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftHelper.kt @@ -0,0 +1,209 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.drafts + +import android.content.Context +import android.net.Uri +import android.util.Log +import android.webkit.MimeTypeMap +import androidx.core.content.FileProvider +import androidx.core.net.toUri +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.entity.DraftAttachment +import com.keylesspalace.tusky.db.entity.DraftEntity +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.NewPoll +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.util.copyToFile +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.File +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import okhttp3.OkHttpClient +import okhttp3.Request +import okio.buffer +import okio.sink + +class DraftHelper @Inject constructor( + @ApplicationContext val context: Context, + private val okHttpClient: OkHttpClient, + db: AppDatabase +) { + + private val draftDao = db.draftDao() + + suspend fun saveDraft( + draftId: Int, + accountId: Long, + inReplyToId: String?, + content: String?, + contentWarning: String?, + sensitive: Boolean, + visibility: Status.Visibility, + mediaUris: List<String>, + mediaDescriptions: List<String?>, + mediaFocus: List<Attachment.Focus?>, + poll: NewPoll?, + failedToSend: Boolean, + failedToSendAlert: Boolean, + scheduledAt: String?, + language: String?, + statusId: String? + ) = withContext(Dispatchers.IO) { + val externalFilesDir = context.getExternalFilesDir("Tusky") + + if (externalFilesDir == null || !(externalFilesDir.exists())) { + Log.e("DraftHelper", "Error obtaining directory to save media.") + throw Exception() + } + + val draftDirectory = File(externalFilesDir, "Drafts") + + if (!draftDirectory.exists()) { + draftDirectory.mkdir() + } + + val uris = mediaUris.map { uriString -> + uriString.toUri() + }.mapIndexedNotNull { index, uri -> + if (uri.isInFolder(draftDirectory)) { + uri + } else { + uri.copyToFolder(draftDirectory, index) + } + } + + val types = uris.map { uri -> + val mimeType = context.contentResolver.getType(uri) + when (mimeType?.substring(0, mimeType.indexOf('/'))) { + "video" -> DraftAttachment.Type.VIDEO + "image" -> DraftAttachment.Type.IMAGE + "audio" -> DraftAttachment.Type.AUDIO + else -> throw IllegalStateException("unknown media type") + } + } + + val attachments: List<DraftAttachment> = buildList(mediaUris.size) { + for (i in mediaUris.indices) { + add( + DraftAttachment( + uriString = uris[i].toString(), + description = mediaDescriptions[i], + focus = mediaFocus[i], + type = types[i] + ) + ) + } + } + + val draft = DraftEntity( + id = draftId, + accountId = accountId, + inReplyToId = inReplyToId, + content = content, + contentWarning = contentWarning, + sensitive = sensitive, + visibility = visibility, + attachments = attachments, + poll = poll, + failedToSend = failedToSend, + failedToSendNew = failedToSendAlert, + scheduledAt = scheduledAt, + language = language, + statusId = statusId + ) + + draftDao.insertOrReplace(draft) + Log.d("DraftHelper", "saved draft to db") + } + + suspend fun deleteDraftAndAttachments(draftId: Int) { + draftDao.find(draftId)?.let { draft -> + deleteDraftAndAttachments(draft) + } + } + + private suspend fun deleteDraftAndAttachments(draft: DraftEntity) { + deleteAttachments(draft) + draftDao.delete(draft.id) + } + + suspend fun deleteAllDraftsAndAttachmentsForAccount(accountId: Long) { + draftDao.loadDrafts(accountId).forEach { draft -> + deleteDraftAndAttachments(draft) + } + } + + suspend fun deleteAttachments(draft: DraftEntity) = withContext(Dispatchers.IO) { + draft.attachments.forEach { attachment -> + if (context.contentResolver.delete(attachment.uri, null, null) == 0) { + Log.e("DraftHelper", "Did not delete file ${attachment.uriString}") + } + } + } + + private fun Uri.isInFolder(folder: File): Boolean { + val filePath = path ?: return true + return File(filePath).parentFile == folder + } + + private fun Uri.copyToFolder(folder: File, index: Int): Uri? { + val contentResolver = context.contentResolver + val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(Date()) + + val fileExtension = if (scheme == "https") { + lastPathSegment?.substringAfterLast('.', "tmp") + } else { + val mimeType = contentResolver.getType(this) + val map = MimeTypeMap.getSingleton() + map.getExtensionFromMimeType(mimeType) + } + + val filename = String.format("Tusky_Draft_Media_%s_%d.%s", timeStamp, index, fileExtension) + val file = File(folder, filename) + + if (scheme == "https") { + // saving redrafted media + try { + val request = Request.Builder().url(toString()).build() + + val response = okHttpClient.newCall(request).execute() + + file.sink().buffer().use { output -> + response.body?.source()?.use { input -> + output.writeAll(input) + } + } + } catch (ex: IOException) { + Log.w("DraftHelper", "failed to save media", ex) + return null + } + } else { + this.copyToFile(contentResolver, file) + } + return FileProvider.getUriForFile( + context, + BuildConfig.APPLICATION_ID + ".fileprovider", + file + ) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt new file mode 100644 index 0000000..ac63172 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftMediaAdapter.kt @@ -0,0 +1,97 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.drafts + +import android.view.ViewGroup +import android.widget.ImageView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.DiskCacheStrategy +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.db.entity.DraftAttachment +import com.keylesspalace.tusky.view.MediaPreviewImageView + +class DraftMediaAdapter( + private val attachmentClick: () -> Unit +) : ListAdapter<DraftAttachment, DraftMediaAdapter.DraftMediaViewHolder>( + object : DiffUtil.ItemCallback<DraftAttachment>() { + override fun areItemsTheSame(oldItem: DraftAttachment, newItem: DraftAttachment): Boolean { + return oldItem == newItem + } + + override fun areContentsTheSame( + oldItem: DraftAttachment, + newItem: DraftAttachment + ): Boolean { + return oldItem == newItem + } + } +) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DraftMediaViewHolder { + return DraftMediaViewHolder(MediaPreviewImageView(parent.context)) + } + + override fun onBindViewHolder(holder: DraftMediaViewHolder, position: Int) { + getItem(position)?.let { attachment -> + if (attachment.type == DraftAttachment.Type.AUDIO) { + holder.imageView.clearFocus() + holder.imageView.setImageResource(R.drawable.ic_music_box_preview_24dp) + } else { + if (attachment.focus != null) { + holder.imageView.setFocalPoint(attachment.focus) + } else { + holder.imageView.clearFocus() + } + var glide = Glide.with(holder.itemView.context) + .load(attachment.uri) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .dontAnimate() + .centerInside() + + if (attachment.focus != null) { + glide = glide.addListener(holder.imageView) + } + + glide.into(holder.imageView) + } + } + } + + inner class DraftMediaViewHolder(val imageView: MediaPreviewImageView) : + RecyclerView.ViewHolder(imageView) { + init { + val thumbnailViewSize = + imageView.context.resources.getDimensionPixelSize( + R.dimen.compose_media_preview_size + ) + val layoutParams = ConstraintLayout.LayoutParams(thumbnailViewSize, thumbnailViewSize) + val margin = itemView.context.resources + .getDimensionPixelSize(R.dimen.compose_media_preview_margin) + val marginBottom = itemView.context.resources + .getDimensionPixelSize(R.dimen.compose_media_preview_margin_bottom) + layoutParams.setMargins(margin, 0, margin, marginBottom) + imageView.layoutParams = layoutParams + imageView.scaleType = ImageView.ScaleType.CENTER_CROP + imageView.setOnClickListener { + attachmentClick() + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt new file mode 100644 index 0000000..d6668c1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsActivity.kt @@ -0,0 +1,188 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.drafts + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.util.Log +import android.widget.LinearLayout +import android.widget.Toast +import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import at.connyduck.calladapter.networkresult.fold +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.databinding.ActivityDraftsBinding +import com.keylesspalace.tusky.db.DraftsAlert +import com.keylesspalace.tusky.db.entity.DraftEntity +import com.keylesspalace.tusky.util.isHttpNotFound +import com.keylesspalace.tusky.util.parseAsMastodonHtml +import com.keylesspalace.tusky.util.visible +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class DraftsActivity : BaseActivity(), DraftActionListener { + + @Inject + lateinit var draftsAlert: DraftsAlert + + private val viewModel: DraftsViewModel by viewModels() + + private lateinit var binding: ActivityDraftsBinding + private lateinit var bottomSheet: BottomSheetBehavior<LinearLayout> + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + binding = ActivityDraftsBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.apply { + title = getString(R.string.title_drafts) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + binding.draftsErrorMessageView.setup(R.drawable.elephant_friend_empty, R.string.no_drafts) + + val adapter = DraftsAdapter(this) + + binding.draftsRecyclerView.adapter = adapter + binding.draftsRecyclerView.layoutManager = LinearLayoutManager(this) + binding.draftsRecyclerView.addItemDecoration( + DividerItemDecoration(this, DividerItemDecoration.VERTICAL) + ) + + bottomSheet = BottomSheetBehavior.from(binding.bottomSheet.root) + + lifecycleScope.launch { + viewModel.drafts.collectLatest { draftData -> + adapter.submitData(draftData) + } + } + + adapter.addLoadStateListener { + binding.draftsErrorMessageView.visible(adapter.itemCount == 0) + } + + // If a failed post is saved to drafts while this activity is up, do nothing; the user is already in the drafts view. + draftsAlert.observeInContext(this, false) + } + + override fun onOpenDraft(draft: DraftEntity) { + if (draft.inReplyToId == null) { + openDraftWithoutReply(draft) + return + } + + val context = this as Context + + lifecycleScope.launch { + bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED + viewModel.getStatus(draft.inReplyToId) + .fold( + { status -> + val composeOptions = ComposeActivity.ComposeOptions( + draftId = draft.id, + content = draft.content, + contentWarning = draft.contentWarning, + inReplyToId = draft.inReplyToId, + replyingStatusContent = status.content.parseAsMastodonHtml().toString(), + replyingStatusAuthor = status.account.localUsername, + draftAttachments = draft.attachments, + poll = draft.poll, + sensitive = draft.sensitive, + visibility = draft.visibility, + scheduledAt = draft.scheduledAt, + language = draft.language, + statusId = draft.statusId, + kind = ComposeActivity.ComposeKind.EDIT_DRAFT + ) + + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + + startActivity(ComposeActivity.startIntent(context, composeOptions)) + }, + { throwable -> + bottomSheet.state = BottomSheetBehavior.STATE_HIDDEN + + Log.w(TAG, "failed loading reply information", throwable) + + if (throwable.isHttpNotFound()) { + // the original status to which a reply was drafted has been deleted + // let's open the ComposeActivity without reply information + Toast.makeText( + context, + getString(R.string.drafts_post_reply_removed), + Toast.LENGTH_LONG + ).show() + openDraftWithoutReply(draft) + } else { + Snackbar.make( + binding.root, + getString(R.string.drafts_failed_loading_reply), + Snackbar.LENGTH_SHORT + ) + .show() + } + } + ) + } + } + + private fun openDraftWithoutReply(draft: DraftEntity) { + val composeOptions = ComposeActivity.ComposeOptions( + draftId = draft.id, + content = draft.content, + contentWarning = draft.contentWarning, + draftAttachments = draft.attachments, + poll = draft.poll, + sensitive = draft.sensitive, + visibility = draft.visibility, + scheduledAt = draft.scheduledAt, + language = draft.language, + statusId = draft.statusId, + kind = ComposeActivity.ComposeKind.EDIT_DRAFT + ) + + startActivity(ComposeActivity.startIntent(this, composeOptions)) + } + + override fun onDeleteDraft(draft: DraftEntity) { + viewModel.deleteDraft(draft) + Snackbar.make(binding.root, getString(R.string.draft_deleted), Snackbar.LENGTH_LONG) + .setAction(R.string.action_undo) { + viewModel.restoreDraft(draft) + } + .show() + } + + companion object { + const val TAG = "DraftsActivity" + + fun newIntent(context: Context) = Intent(context, DraftsActivity::class.java) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt new file mode 100644 index 0000000..05d8b82 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsAdapter.kt @@ -0,0 +1,95 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.drafts + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.databinding.ItemDraftBinding +import com.keylesspalace.tusky.db.entity.DraftEntity +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.visible + +interface DraftActionListener { + fun onOpenDraft(draft: DraftEntity) + fun onDeleteDraft(draft: DraftEntity) +} + +class DraftsAdapter( + private val listener: DraftActionListener +) : PagingDataAdapter<DraftEntity, BindingHolder<ItemDraftBinding>>( + object : DiffUtil.ItemCallback<DraftEntity>() { + override fun areItemsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: DraftEntity, newItem: DraftEntity): Boolean { + return oldItem == newItem + } + } +) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemDraftBinding> { + val binding = ItemDraftBinding.inflate(LayoutInflater.from(parent.context), parent, false) + + val viewHolder = BindingHolder(binding) + + binding.draftMediaPreview.layoutManager = LinearLayoutManager(binding.root.context, RecyclerView.HORIZONTAL, false) + binding.draftMediaPreview.adapter = DraftMediaAdapter { + getItem(viewHolder.bindingAdapterPosition)?.let { draft -> + listener.onOpenDraft(draft) + } + } + + return viewHolder + } + + override fun onBindViewHolder(holder: BindingHolder<ItemDraftBinding>, position: Int) { + getItem(position)?.let { draft -> + holder.binding.root.setOnClickListener { + listener.onOpenDraft(draft) + } + holder.binding.deleteButton.setOnClickListener { + listener.onDeleteDraft(draft) + } + holder.binding.draftSendingInfo.visible(draft.failedToSend) + + holder.binding.contentWarning.visible(!draft.contentWarning.isNullOrEmpty()) + holder.binding.contentWarning.text = draft.contentWarning + holder.binding.content.text = draft.content + + holder.binding.draftMediaPreview.visible(draft.attachments.isNotEmpty()) + (holder.binding.draftMediaPreview.adapter as DraftMediaAdapter).submitList( + draft.attachments + ) + + if (draft.poll != null) { + holder.binding.draftPoll.show() + holder.binding.draftPoll.setPoll(draft.poll) + } else { + holder.binding.draftPoll.hide() + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt new file mode 100644 index 0000000..b56985c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/drafts/DraftsViewModel.kt @@ -0,0 +1,82 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.drafts + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import at.connyduck.calladapter.networkresult.NetworkResult +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.entity.DraftEntity +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch + +@HiltViewModel +class DraftsViewModel @Inject constructor( + val database: AppDatabase, + val accountManager: AccountManager, + val api: MastodonApi, + private val draftHelper: DraftHelper +) : ViewModel() { + + val drafts = Pager( + config = PagingConfig( + pageSize = 20 + ), + pagingSourceFactory = { + database.draftDao().draftsPagingSource( + accountManager.activeAccount?.id!! + ) + } + ).flow + .cachedIn(viewModelScope) + + private val deletedDrafts: MutableList<DraftEntity> = mutableListOf() + + fun deleteDraft(draft: DraftEntity) { + // this does not immediately delete media files to avoid unnecessary file operations + // in case the user decides to restore the draft + viewModelScope.launch { + database.draftDao().delete(draft.id) + deletedDrafts.add(draft) + } + } + + fun restoreDraft(draft: DraftEntity) { + viewModelScope.launch { + database.draftDao().insertOrReplace(draft) + deletedDrafts.remove(draft) + } + } + + suspend fun getStatus(statusId: String): NetworkResult<Status> { + return api.status(statusId) + } + + override fun onCleared() { + viewModelScope.launch { + deletedDrafts.forEach { + draftHelper.deleteAttachments(it) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt new file mode 100644 index 0000000..7a0f9dd --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterActivity.kt @@ -0,0 +1,337 @@ +package com.keylesspalace.tusky.components.filters + +import android.content.Context +import android.content.DialogInterface.BUTTON_POSITIVE +import android.os.Bundle +import android.view.View +import android.widget.AdapterView +import android.widget.ArrayAdapter +import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog +import androidx.core.view.size +import androidx.core.widget.doAfterTextChanged +import androidx.lifecycle.lifecycleScope +import at.connyduck.calladapter.networkresult.fold +import com.google.android.material.chip.Chip +import com.google.android.material.snackbar.Snackbar +import com.google.android.material.switchmaterial.SwitchMaterial +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.FilterUpdatedEvent +import com.keylesspalace.tusky.databinding.ActivityEditFilterBinding +import com.keylesspalace.tusky.databinding.DialogFilterBinding +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.FilterKeyword +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.getParcelableExtraCompat +import com.keylesspalace.tusky.util.isHttpNotFound +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import dagger.hilt.android.AndroidEntryPoint +import java.util.Date +import javax.inject.Inject +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class EditFilterActivity : BaseActivity() { + @Inject + lateinit var api: MastodonApi + + @Inject + lateinit var eventHub: EventHub + + private val binding by viewBinding(ActivityEditFilterBinding::inflate) + private val viewModel: EditFilterViewModel by viewModels() + + private lateinit var filter: Filter + private var originalFilter: Filter? = null + private lateinit var contextSwitches: Map<SwitchMaterial, Filter.Kind> + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + originalFilter = intent.getParcelableExtraCompat(FILTER_TO_EDIT) + filter = originalFilter ?: Filter("", "", listOf(), null, Filter.Action.WARN.action, listOf()) + binding.apply { + contextSwitches = mapOf( + filterContextHome to Filter.Kind.HOME, + filterContextNotifications to Filter.Kind.NOTIFICATIONS, + filterContextPublic to Filter.Kind.PUBLIC, + filterContextThread to Filter.Kind.THREAD, + filterContextAccount to Filter.Kind.ACCOUNT + ) + } + + setContentView(binding.root) + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.run { + // Back button + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + setTitle( + if (originalFilter == null) { + R.string.filter_addition_title + } else { + R.string.filter_edit_title + } + ) + + binding.actionChip.setOnClickListener { showAddKeywordDialog() } + binding.filterSaveButton.setOnClickListener { saveChanges() } + binding.filterDeleteButton.setOnClickListener { + lifecycleScope.launch { + if (showDeleteFilterDialog(filter.title) == BUTTON_POSITIVE) deleteFilter() + } + } + binding.filterDeleteButton.visible(originalFilter != null) + + for (switch in contextSwitches.keys) { + switch.setOnCheckedChangeListener { _, isChecked -> + val context = contextSwitches[switch]!! + if (isChecked) { + viewModel.addContext(context) + } else { + viewModel.removeContext(context) + } + validateSaveButton() + } + } + binding.filterTitle.doAfterTextChanged { editable -> + viewModel.setTitle(editable.toString()) + validateSaveButton() + } + binding.filterActionWarn.setOnCheckedChangeListener { _, checked -> + viewModel.setAction( + if (checked) { + Filter.Action.WARN + } else { + Filter.Action.HIDE + } + ) + } + binding.filterDurationSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { + viewModel.setDuration( + if (originalFilter?.expiresAt == null) { + position + } else { + position - 1 + } + ) + } + + override fun onNothingSelected(parent: AdapterView<*>?) { + viewModel.setDuration(0) + } + } + validateSaveButton() + + if (originalFilter == null) { + binding.filterActionWarn.isChecked = true + } else { + loadFilter() + } + observeModel() + } + + private fun observeModel() { + lifecycleScope.launch { + viewModel.title.collect { title -> + if (title != binding.filterTitle.text.toString()) { + // We also get this callback when typing in the field, + // which messes with the cursor focus + binding.filterTitle.setText(title) + } + } + } + lifecycleScope.launch { + viewModel.keywords.collect { keywords -> + updateKeywords(keywords) + } + } + lifecycleScope.launch { + viewModel.contexts.collect { contexts -> + for (entry in contextSwitches) { + entry.key.isChecked = contexts.contains(entry.value) + } + } + } + lifecycleScope.launch { + viewModel.action.collect { action -> + when (action) { + Filter.Action.HIDE -> binding.filterActionHide.isChecked = true + else -> binding.filterActionWarn.isChecked = true + } + } + } + } + + // Populate the UI from the filter's members + private fun loadFilter() { + viewModel.load(filter) + if (filter.expiresAt != null) { + val durationNames = listOf(getString(R.string.duration_no_change)) + resources.getStringArray(R.array.filter_duration_names) + binding.filterDurationSpinner.adapter = ArrayAdapter(this, android.R.layout.simple_list_item_1, durationNames) + } + } + + private fun updateKeywords(newKeywords: List<FilterKeyword>) { + newKeywords.forEachIndexed { index, filterKeyword -> + val chip = binding.keywordChips.getChildAt(index).takeUnless { + it.id == R.id.actionChip + } as Chip? ?: Chip(this).apply { + setCloseIconResource(R.drawable.ic_cancel_24dp) + isCheckable = false + binding.keywordChips.addView(this, binding.keywordChips.size - 1) + } + + chip.text = if (filterKeyword.wholeWord) { + binding.root.context.getString( + R.string.filter_keyword_display_format, + filterKeyword.keyword + ) + } else { + filterKeyword.keyword + } + chip.isCloseIconVisible = true + chip.setOnClickListener { + showEditKeywordDialog(newKeywords[index]) + } + chip.setOnCloseIconClickListener { + viewModel.deleteKeyword(newKeywords[index]) + } + } + + while (binding.keywordChips.size - 1 > newKeywords.size) { + binding.keywordChips.removeViewAt(newKeywords.size) + } + + filter = filter.copy(keywords = newKeywords) + validateSaveButton() + } + + private fun showAddKeywordDialog() { + val binding = DialogFilterBinding.inflate(layoutInflater) + binding.phraseWholeWord.isChecked = true + AlertDialog.Builder(this) + .setTitle(R.string.filter_keyword_addition_title) + .setView(binding.root) + .setPositiveButton(android.R.string.ok) { _, _ -> + viewModel.addKeyword( + FilterKeyword( + "", + binding.phraseEditText.text.toString(), + binding.phraseWholeWord.isChecked + ) + ) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun showEditKeywordDialog(keyword: FilterKeyword) { + val binding = DialogFilterBinding.inflate(layoutInflater) + binding.phraseEditText.setText(keyword.keyword) + binding.phraseWholeWord.isChecked = keyword.wholeWord + + AlertDialog.Builder(this) + .setTitle(R.string.filter_edit_keyword_title) + .setView(binding.root) + .setPositiveButton(R.string.filter_dialog_update_button) { _, _ -> + viewModel.modifyKeyword( + keyword, + keyword.copy( + keyword = binding.phraseEditText.text.toString(), + wholeWord = binding.phraseWholeWord.isChecked + ) + ) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun validateSaveButton() { + binding.filterSaveButton.isEnabled = viewModel.validate() + } + + private fun saveChanges() { + // TODO use a progress bar here (see EditProfileActivity/activity_edit_profile.xml for example)? + + lifecycleScope.launch { + if (viewModel.saveChanges(this@EditFilterActivity)) { + finish() + // Possibly affected contexts: any context affected by the original filter OR any context affected by the updated filter + val affectedContexts = viewModel.contexts.value.map { + it.kind + }.union(originalFilter?.context ?: listOf()).distinct() + eventHub.dispatch(FilterUpdatedEvent(affectedContexts)) + } else { + Snackbar.make( + binding.root, + getString(R.string.error_deleting_filter, viewModel.title.value), + Snackbar.LENGTH_SHORT + ).show() + } + } + } + + private fun deleteFilter() { + originalFilter?.let { filter -> + lifecycleScope.launch { + api.deleteFilter(filter.id).fold( + { + finish() + }, + { throwable -> + if (throwable.isHttpNotFound()) { + api.deleteFilterV1(filter.id).fold( + { + finish() + }, + { + Snackbar.make( + binding.root, + getString(R.string.error_deleting_filter, filter.title), + Snackbar.LENGTH_SHORT + ).show() + } + ) + } else { + Snackbar.make( + binding.root, + getString(R.string.error_deleting_filter, filter.title), + Snackbar.LENGTH_SHORT + ).show() + } + } + ) + } + } + } + + companion object { + const val FILTER_TO_EDIT = "FilterToEdit" + + // Mastodon *stores* the absolute date in the filter, + // but create/edit take a number of seconds (relative to the time the operation is posted) + fun getSecondsForDurationIndex(index: Int, context: Context?, default: Date? = null): Int? { + return when (index) { + -1 -> if (default == null) { + default + } else { + ((default.time - System.currentTimeMillis()) / 1000).toInt() + } + 0 -> null + else -> context?.resources?.getIntArray(R.array.filter_duration_values)?.get(index) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt new file mode 100644 index 0000000..9bde721 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/EditFilterViewModel.kt @@ -0,0 +1,216 @@ +package com.keylesspalace.tusky.components.filters + +import android.content.Context +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.FilterKeyword +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.isHttpNotFound +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.withContext + +@HiltViewModel +class EditFilterViewModel @Inject constructor(val api: MastodonApi) : ViewModel() { + private var originalFilter: Filter? = null + + private val _title = MutableStateFlow("") + val title: StateFlow<String> = _title.asStateFlow() + + private val _keywords = MutableStateFlow(listOf<FilterKeyword>()) + val keywords: StateFlow<List<FilterKeyword>> = _keywords.asStateFlow() + + private val _action = MutableStateFlow(Filter.Action.WARN) + val action: StateFlow<Filter.Action> = _action.asStateFlow() + + private val _duration = MutableStateFlow(0) + val duration: StateFlow<Int> = _duration.asStateFlow() + + private val _contexts = MutableStateFlow(listOf<Filter.Kind>()) + val contexts: StateFlow<List<Filter.Kind>> = _contexts.asStateFlow() + + fun load(filter: Filter) { + originalFilter = filter + _title.value = filter.title + _keywords.value = filter.keywords + _action.value = filter.action + _duration.value = if (filter.expiresAt == null) { + 0 + } else { + -1 + } + _contexts.value = filter.kinds + } + + fun addKeyword(keyword: FilterKeyword) { + _keywords.value += keyword + } + + fun deleteKeyword(keyword: FilterKeyword) { + _keywords.value = _keywords.value.filterNot { it == keyword } + } + + fun modifyKeyword(original: FilterKeyword, updated: FilterKeyword) { + val index = _keywords.value.indexOf(original) + if (index >= 0) { + _keywords.value = _keywords.value.toMutableList().apply { + set(index, updated) + } + } + } + + fun setTitle(title: String) { + this._title.value = title + } + + fun setDuration(index: Int) { + _duration.value = index + } + + fun setAction(action: Filter.Action) { + this._action.value = action + } + + fun addContext(context: Filter.Kind) { + if (!_contexts.value.contains(context)) { + _contexts.value += context + } + } + + fun removeContext(context: Filter.Kind) { + _contexts.value = _contexts.value.filter { it != context } + } + + fun validate(): Boolean { + return _title.value.isNotBlank() && + _keywords.value.isNotEmpty() && + _contexts.value.isNotEmpty() + } + + suspend fun saveChanges(context: Context): Boolean { + val contexts = _contexts.value.map { it.kind } + val title = _title.value + val durationIndex = _duration.value + val action = _action.value.action + + return withContext(viewModelScope.coroutineContext) { + originalFilter?.let { filter -> + updateFilter(filter, title, contexts, action, durationIndex, context) + } ?: createFilter(title, contexts, action, durationIndex, context) + } + } + + private suspend fun createFilter( + title: String, + contexts: List<String>, + action: String, + durationIndex: Int, + context: Context + ): Boolean { + val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context) + api.createFilter( + title = title, + context = contexts, + filterAction = action, + expiresInSeconds = expiresInSeconds + ).fold( + { newFilter -> + // This is _terrible_, but the all-in-one update filter api Just Doesn't Work + return _keywords.value.map { keyword -> + api.addFilterKeyword( + filterId = newFilter.id, + keyword = keyword.keyword, + wholeWord = keyword.wholeWord + ) + }.none { it.isFailure } + }, + { throwable -> + return ( + throwable.isHttpNotFound() && + // Endpoint not found, fall back to v1 api + createFilterV1(contexts, expiresInSeconds) + ) + } + ) + } + + private suspend fun updateFilter( + originalFilter: Filter, + title: String, + contexts: List<String>, + action: String, + durationIndex: Int, + context: Context + ): Boolean { + val expiresInSeconds = EditFilterActivity.getSecondsForDurationIndex(durationIndex, context) + api.updateFilter( + id = originalFilter.id, + title = title, + context = contexts, + filterAction = action, + expiresInSeconds = expiresInSeconds + ).fold( + { + // This is _terrible_, but the all-in-one update filter api Just Doesn't Work + val results = _keywords.value.map { keyword -> + if (keyword.id.isEmpty()) { + api.addFilterKeyword(filterId = originalFilter.id, keyword = keyword.keyword, wholeWord = keyword.wholeWord) + } else { + api.updateFilterKeyword(keywordId = keyword.id, keyword = keyword.keyword, wholeWord = keyword.wholeWord) + } + } + originalFilter.keywords.filter { keyword -> + // Deleted keywords + _keywords.value.none { it.id == keyword.id } + }.map { api.deleteFilterKeyword(it.id) } + + return results.none { it.isFailure } + }, + { throwable -> + if (throwable.isHttpNotFound()) { + // Endpoint not found, fall back to v1 api + if (updateFilterV1(contexts, expiresInSeconds)) { + return true + } + } + return false + } + ) + } + + private suspend fun createFilterV1(context: List<String>, expiresInSeconds: Int?): Boolean { + return _keywords.value.map { keyword -> + api.createFilterV1(keyword.keyword, context, false, keyword.wholeWord, expiresInSeconds) + }.none { it.isFailure } + } + + private suspend fun updateFilterV1(context: List<String>, expiresInSeconds: Int?): Boolean { + val results = _keywords.value.map { keyword -> + if (originalFilter == null) { + api.createFilterV1( + phrase = keyword.keyword, + context = context, + irreversible = false, + wholeWord = keyword.wholeWord, + expiresInSeconds = expiresInSeconds + ) + } else { + api.updateFilterV1( + id = originalFilter!!.id, + phrase = keyword.keyword, + context = context, + irreversible = false, + wholeWord = keyword.wholeWord, + expiresInSeconds = expiresInSeconds + ) + } + } + // Don't handle deleted keywords here because there's only one keyword per v1 filter anyway + + return results.none { it.isFailure } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/FilterExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/FilterExtensions.kt new file mode 100644 index 0000000..0f14bc5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/FilterExtensions.kt @@ -0,0 +1,31 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.components.filters + +import android.app.Activity +import androidx.appcompat.app.AlertDialog +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.util.await + +internal suspend fun Activity.showDeleteFilterDialog(filterTitle: String) = AlertDialog.Builder( + this +) + .setMessage(getString(R.string.dialog_delete_filter_text, filterTitle)) + .setCancelable(true) + .create() + .await(R.string.dialog_delete_filter_positive_action, android.R.string.cancel) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersActivity.kt new file mode 100644 index 0000000..edfa81a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersActivity.kt @@ -0,0 +1,134 @@ +package com.keylesspalace.tusky.components.filters + +import android.content.DialogInterface.BUTTON_POSITIVE +import android.content.Intent +import android.os.Bundle +import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ActivityFiltersBinding +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.launchAndRepeatOnLifecycle +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.util.withSlideInAnimation +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class FiltersActivity : BaseActivity(), FiltersListener { + + private val binding by viewBinding(ActivityFiltersBinding::inflate) + private val viewModel: FiltersViewModel by viewModels() + + private val editFilterLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + // refresh the filters upon returning from EditFilterActivity + reloadFilters() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(binding.root) + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.run { + // Back button + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + binding.addFilterButton.setOnClickListener { + launchEditFilterActivity() + } + + binding.swipeRefreshLayout.setOnRefreshListener { reloadFilters() } + + setTitle(R.string.pref_title_timeline_filters) + + binding.filtersList.addItemDecoration( + DividerItemDecoration(this, DividerItemDecoration.VERTICAL) + ) + + observeViewModel() + } + + private fun observeViewModel() { + launchAndRepeatOnLifecycle { + viewModel.state.collect { state -> + binding.progressBar.visible( + state.loadingState == FiltersViewModel.LoadingState.LOADING + ) + binding.swipeRefreshLayout.isRefreshing = state.loadingState == FiltersViewModel.LoadingState.LOADING + binding.addFilterButton.visible( + state.loadingState == FiltersViewModel.LoadingState.LOADED + ) + + when (state.loadingState) { + FiltersViewModel.LoadingState.INITIAL, FiltersViewModel.LoadingState.LOADING -> binding.messageView.hide() + FiltersViewModel.LoadingState.ERROR_NETWORK -> { + binding.messageView.setup( + R.drawable.errorphant_offline, + R.string.error_network + ) { + reloadFilters() + } + binding.messageView.show() + } + FiltersViewModel.LoadingState.ERROR_OTHER -> { + binding.messageView.setup( + R.drawable.errorphant_error, + R.string.error_generic + ) { + reloadFilters() + } + binding.messageView.show() + } + FiltersViewModel.LoadingState.LOADED -> { + binding.filtersList.adapter = FiltersAdapter(this@FiltersActivity, state.filters) + if (state.filters.isEmpty()) { + binding.messageView.setup( + R.drawable.elephant_friend_empty, + R.string.message_empty, + null + ) + binding.messageView.show() + } else { + binding.messageView.hide() + } + } + } + } + } + } + + private fun reloadFilters() { + viewModel.reload() + } + + private fun launchEditFilterActivity(filter: Filter? = null) { + val intent = Intent(this, EditFilterActivity::class.java).apply { + if (filter != null) { + putExtra(EditFilterActivity.FILTER_TO_EDIT, filter) + } + }.withSlideInAnimation() + editFilterLauncher.launch(intent) + } + + override fun deleteFilter(filter: Filter) { + lifecycleScope.launch { + if (showDeleteFilterDialog(filter.title) == BUTTON_POSITIVE) { + viewModel.deleteFilter(filter, binding.root) + } + } + } + + override fun updateFilter(updatedFilter: Filter) { + launchEditFilterActivity(updatedFilter) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersAdapter.kt new file mode 100644 index 0000000..96d51fc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersAdapter.kt @@ -0,0 +1,57 @@ +package com.keylesspalace.tusky.components.filters + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemRemovableBinding +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.getRelativeTimeSpanString + +class FiltersAdapter(val listener: FiltersListener, val filters: List<Filter>) : + RecyclerView.Adapter<BindingHolder<ItemRemovableBinding>>() { + + override fun getItemCount(): Int = filters.size + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemRemovableBinding> { + return BindingHolder( + ItemRemovableBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + } + + override fun onBindViewHolder(holder: BindingHolder<ItemRemovableBinding>, position: Int) { + val binding = holder.binding + val resources = binding.root.resources + val actions = resources.getStringArray(R.array.filter_actions) + val contexts = resources.getStringArray(R.array.filter_contexts) + + val filter = filters[position] + val context = binding.root.context + binding.textPrimary.text = if (filter.expiresAt == null) { + filter.title + } else { + context.getString( + R.string.filter_expiration_format, + filter.title, + getRelativeTimeSpanString(binding.root.context, filter.expiresAt.time, System.currentTimeMillis()) + ) + } + binding.textSecondary.text = context.getString( + R.string.filter_description_format, + actions.getOrNull(filter.action.ordinal - 1), + filter.context.map { contexts.getOrNull(Filter.Kind.from(it).ordinal) }.joinToString("/") + ) + + binding.delete.setOnClickListener { + listener.deleteFilter(filter) + } + + binding.root.setOnClickListener { + listener.updateFilter(filter) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersListener.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersListener.kt new file mode 100644 index 0000000..a102b0d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersListener.kt @@ -0,0 +1,8 @@ +package com.keylesspalace.tusky.components.filters + +import com.keylesspalace.tusky.entity.Filter + +interface FiltersListener { + fun deleteFilter(filter: Filter) + fun updateFilter(updatedFilter: Filter) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersViewModel.kt new file mode 100644 index 0000000..115a7d9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/filters/FiltersViewModel.kt @@ -0,0 +1,133 @@ +package com.keylesspalace.tusky.components.filters + +import android.util.Log +import android.view.View +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.FilterUpdatedEvent +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.isHttpNotFound +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +@HiltViewModel +class FiltersViewModel @Inject constructor( + private val api: MastodonApi, + private val eventHub: EventHub +) : ViewModel() { + + enum class LoadingState { + INITIAL, + LOADING, + LOADED, + ERROR_NETWORK, + ERROR_OTHER + } + + data class State(val filters: List<Filter>, val loadingState: LoadingState) + + private val _state = MutableStateFlow(State(emptyList(), LoadingState.INITIAL)) + val state: StateFlow<State> = _state.asStateFlow() + + private val loadTrigger = MutableStateFlow(0) + + init { + viewModelScope.launch { + observeLoad() + } + } + + private suspend fun observeLoad() { + loadTrigger.collectLatest { + _state.update { it.copy(loadingState = LoadingState.LOADING) } + + api.getFilters().fold( + { filters -> + _state.value = State(filters, LoadingState.LOADED) + }, + { throwable -> + if (throwable.isHttpNotFound()) { + Log.i(TAG, "failed loading filters v2, falling back to v1", throwable) + + api.getFiltersV1().fold( + { filters -> + _state.value = State(filters.map { it.toFilter() }, LoadingState.LOADED) + }, + { t -> + Log.w(TAG, "failed loading filters v1", t) + _state.value = State(emptyList(), LoadingState.ERROR_OTHER) + } + ) + } else { + Log.w(TAG, "failed loading filters v2", throwable) + _state.update { it.copy(loadingState = LoadingState.ERROR_NETWORK) } + } + } + ) + } + } + + fun reload() { + loadTrigger.update { it + 1 } + } + + suspend fun deleteFilter(filter: Filter, parent: View) { + // First wait for a pending loading operation to complete + _state.first { it.loadingState > LoadingState.LOADING } + + api.deleteFilter(filter.id).fold( + { + _state.update { currentState -> + State( + currentState.filters.filter { it.id != filter.id }, + LoadingState.LOADED + ) + } + eventHub.dispatch(FilterUpdatedEvent(filter.context)) + }, + { throwable -> + if (throwable.isHttpNotFound()) { + api.deleteFilterV1(filter.id).fold( + { + _state.update { currentState -> + State( + currentState.filters.filter { it.id != filter.id }, + LoadingState.LOADED + ) + } + }, + { + Snackbar.make( + parent, + parent.context.getString(R.string.error_deleting_filter, filter.title), + Snackbar.LENGTH_SHORT + ).show() + } + ) + } else { + Snackbar.make( + parent, + parent.context.getString(R.string.error_deleting_filter, filter.title), + Snackbar.LENGTH_SHORT + ).show() + } + } + ) + } + + companion object { + private const val TAG = "FiltersViewModel" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt new file mode 100644 index 0000000..2be4760 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsActivity.kt @@ -0,0 +1,214 @@ +package com.keylesspalace.tusky.components.followedtags + +import android.app.Dialog +import android.content.DialogInterface +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import android.widget.AutoCompleteTextView +import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog +import androidx.fragment.app.DialogFragment +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.SimpleItemAnimator +import at.connyduck.calladapter.networkresult.fold +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.StatusListActivity +import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter +import com.keylesspalace.tusky.databinding.ActivityFollowedTagsBinding +import com.keylesspalace.tusky.interfaces.HashtagActionListener +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.copyToClipboard +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class FollowedTagsActivity : + BaseActivity(), + HashtagActionListener, + ComposeAutoCompleteAdapter.AutocompletionProvider { + @Inject + lateinit var api: MastodonApi + + @Inject + lateinit var sharedPreferences: SharedPreferences + + private val binding by viewBinding(ActivityFollowedTagsBinding::inflate) + private val viewModel: FollowedTagsViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(binding.root) + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.run { + setTitle(R.string.title_followed_hashtags) + // Back button + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + binding.fab.setOnClickListener { + val dialog: DialogFragment = FollowTagDialog.newInstance() + dialog.show(supportFragmentManager, "dialog") + } + + setupAdapter().let { adapter -> + setupRecyclerView(adapter) + + lifecycleScope.launch { + viewModel.pager.collectLatest { pagingData -> + adapter.submitData(pagingData) + } + } + } + } + + private fun setupRecyclerView(adapter: FollowedTagsAdapter) { + binding.followedTagsView.adapter = adapter + binding.followedTagsView.setHasFixedSize(true) + binding.followedTagsView.layoutManager = LinearLayoutManager(this) + binding.followedTagsView.addItemDecoration( + DividerItemDecoration(this, DividerItemDecoration.VERTICAL) + ) + (binding.followedTagsView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + } + + private fun setupAdapter(): FollowedTagsAdapter { + return FollowedTagsAdapter(this, viewModel).apply { + addLoadStateListener { loadState -> + binding.followedTagsProgressBar.visible( + loadState.refresh == LoadState.Loading && itemCount == 0 + ) + + if (loadState.refresh is LoadState.Error) { + binding.followedTagsView.hide() + binding.followedTagsMessageView.show() + val errorState = loadState.refresh as LoadState.Error + binding.followedTagsMessageView.setup(errorState.error) { retry() } + Log.w(TAG, "error loading followed hashtags", errorState.error) + } else { + binding.followedTagsView.show() + binding.followedTagsMessageView.hide() + } + } + } + } + + private fun follow(tagName: String, position: Int = -1) { + lifecycleScope.launch { + api.followTag(tagName).fold( + { + if (position == -1) { + viewModel.tags.add(it) + } else { + viewModel.tags.add(position, it) + } + viewModel.currentSource?.invalidate() + }, + { + Snackbar.make( + this@FollowedTagsActivity, + binding.followedTagsView, + getString(R.string.error_following_hashtag_format, tagName), + Snackbar.LENGTH_SHORT + ) + .show() + } + ) + } + } + + override fun unfollow(tagName: String, position: Int) { + lifecycleScope.launch { + api.unfollowTag(tagName).fold( + { + viewModel.tags.removeIf { tag -> tag.name == tagName } + Snackbar.make( + this@FollowedTagsActivity, + binding.followedTagsView, + getString(R.string.confirmation_hashtag_unfollowed, tagName), + Snackbar.LENGTH_LONG + ) + .setAction(R.string.action_undo) { + follow(tagName, position) + } + .show() + viewModel.currentSource?.invalidate() + }, + { + Snackbar.make( + this@FollowedTagsActivity, + binding.followedTagsView, + getString( + R.string.error_unfollowing_hashtag_format, + tagName + ), + Snackbar.LENGTH_SHORT + ) + .show() + } + ) + } + } + + override fun search(token: String): List<ComposeAutoCompleteAdapter.AutocompleteResult> { + return viewModel.searchAutocompleteSuggestions(token) + } + + override fun viewTag(tagName: String) { + startActivity(StatusListActivity.newHashtagIntent(this, tagName)) + } + + override fun copyTagName(tagName: String) { + copyToClipboard( + "#$tagName", + getString(R.string.confirmation_hashtag_copied, tagName), + ) + } + + companion object { + const val TAG = "FollowedTagsActivity" + } + + class FollowTagDialog : DialogFragment() { + override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { + val layout = layoutInflater.inflate(R.layout.dialog_follow_hashtag, null) + val autoCompleteTextView = layout.findViewById<AutoCompleteTextView>(R.id.hashtag)!! + autoCompleteTextView.setAdapter( + ComposeAutoCompleteAdapter( + requireActivity() as FollowedTagsActivity, + animateAvatar = false, + animateEmojis = false, + showBotBadge = false + ) + ) + + return AlertDialog.Builder(requireActivity()) + .setTitle(R.string.dialog_follow_hashtag_title) + .setView(layout) + .setPositiveButton(android.R.string.ok) { _, _ -> + (requireActivity() as FollowedTagsActivity).follow( + autoCompleteTextView.text.toString().removePrefix("#") + ) + } + .setNegativeButton(android.R.string.cancel) { _: DialogInterface, _: Int -> } + .create() + } + + companion object { + fun newInstance(): FollowTagDialog = FollowTagDialog() + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsAdapter.kt new file mode 100644 index 0000000..5df59a5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsAdapter.kt @@ -0,0 +1,59 @@ +package com.keylesspalace.tusky.components.followedtags + +import android.view.LayoutInflater +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.TextView +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemFollowedHashtagBinding +import com.keylesspalace.tusky.interfaces.HashtagActionListener +import com.keylesspalace.tusky.util.BindingHolder + +class FollowedTagsAdapter( + private val actionListener: HashtagActionListener, + private val viewModel: FollowedTagsViewModel +) : PagingDataAdapter<String, BindingHolder<ItemFollowedHashtagBinding>>(STRING_COMPARATOR) { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemFollowedHashtagBinding> = BindingHolder( + ItemFollowedHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + override fun onBindViewHolder( + holder: BindingHolder<ItemFollowedHashtagBinding>, + position: Int + ) { + viewModel.tags[position].let { tag -> + holder.itemView.findViewById<TextView>(R.id.followed_tag).apply { + text = tag.name + setOnClickListener { + actionListener.viewTag(tag.name) + } + setOnLongClickListener { + actionListener.copyTagName(tag.name) + true + } + } + + holder.itemView.findViewById<ImageButton>( + R.id.followed_tag_unfollow + ).setOnClickListener { + actionListener.unfollow(tag.name, holder.bindingAdapterPosition) + } + } + } + + override fun getItemCount(): Int = viewModel.tags.size + + companion object { + val STRING_COMPARATOR = object : DiffUtil.ItemCallback<String>() { + override fun areItemsTheSame(oldItem: String, newItem: String): Boolean = + oldItem == newItem + override fun areContentsTheSame(oldItem: String, newItem: String): Boolean = + oldItem == newItem + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsPagingSource.kt new file mode 100644 index 0000000..da5479c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsPagingSource.kt @@ -0,0 +1,16 @@ +package com.keylesspalace.tusky.components.followedtags + +import androidx.paging.PagingSource +import androidx.paging.PagingState + +class FollowedTagsPagingSource(private val viewModel: FollowedTagsViewModel) : PagingSource<String, String>() { + override fun getRefreshKey(state: PagingState<String, String>): String? = null + + override suspend fun load(params: LoadParams<String>): LoadResult<String, String> { + return if (params is LoadParams.Refresh) { + LoadResult.Page(viewModel.tags.map { it.name }, null, viewModel.nextKey) + } else { + LoadResult.Page(emptyList(), null, null) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsRemoteMediator.kt new file mode 100644 index 0000000..00239a7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsRemoteMediator.kt @@ -0,0 +1,57 @@ +package com.keylesspalace.tusky.components.followedtags + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import com.keylesspalace.tusky.entity.HashTag +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.HttpHeaderLink +import retrofit2.HttpException +import retrofit2.Response + +@OptIn(ExperimentalPagingApi::class) +class FollowedTagsRemoteMediator( + private val api: MastodonApi, + private val viewModel: FollowedTagsViewModel +) : RemoteMediator<String, String>() { + override suspend fun load( + loadType: LoadType, + state: PagingState<String, String> + ): MediatorResult { + return try { + val response = request(loadType) + ?: return MediatorResult.Success(endOfPaginationReached = true) + + return applyResponse(response) + } catch (e: Exception) { + MediatorResult.Error(e) + } + } + + private suspend fun request(loadType: LoadType): Response<List<HashTag>>? { + return when (loadType) { + LoadType.PREPEND -> null + LoadType.APPEND -> api.followedTags(maxId = viewModel.nextKey) + LoadType.REFRESH -> { + viewModel.nextKey = null + viewModel.tags.clear() + api.followedTags() + } + } + } + + private fun applyResponse(response: Response<List<HashTag>>): MediatorResult { + val tags = response.body() + if (!response.isSuccessful || tags == null) { + return MediatorResult.Error(HttpException(response)) + } + + val links = HttpHeaderLink.parse(response.headers()["Link"]) + viewModel.nextKey = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter("max_id") + viewModel.tags.addAll(tags) + viewModel.currentSource?.invalidate() + + return MediatorResult.Success(endOfPaginationReached = viewModel.nextKey == null) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsViewModel.kt new file mode 100644 index 0000000..bebc4ff --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/followedtags/FollowedTagsViewModel.kt @@ -0,0 +1,63 @@ +package com.keylesspalace.tusky.components.followedtags + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import at.connyduck.calladapter.networkresult.fold +import com.keylesspalace.tusky.components.compose.ComposeAutoCompleteAdapter +import com.keylesspalace.tusky.components.search.SearchType +import com.keylesspalace.tusky.entity.HashTag +import com.keylesspalace.tusky.network.MastodonApi +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.runBlocking + +@HiltViewModel +class FollowedTagsViewModel @Inject constructor( + private val api: MastodonApi +) : ViewModel() { + val tags: MutableList<HashTag> = mutableListOf() + var nextKey: String? = null + var currentSource: FollowedTagsPagingSource? = null + + @OptIn(ExperimentalPagingApi::class) + val pager = Pager( + config = PagingConfig( + pageSize = 100 + ), + remoteMediator = FollowedTagsRemoteMediator(api, this), + pagingSourceFactory = { + FollowedTagsPagingSource( + viewModel = this + ).also { source -> + currentSource = source + } + } + ).flow.cachedIn(viewModelScope) + + fun searchAutocompleteSuggestions( + token: String + ): List<ComposeAutoCompleteAdapter.AutocompleteResult> { + return runBlocking { + api.search(query = token, type = SearchType.Hashtag.apiParameter, limit = 10) + .fold({ searchResult -> + searchResult.hashtags.map { + ComposeAutoCompleteAdapter.AutocompleteResult.HashtagResult( + it.name + ) + } + }, { e -> + Log.e(TAG, "Autocomplete search for $token failed.", e) + emptyList() + }) + } + } + + companion object { + private const val TAG = "FollowedTagsViewModel" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt new file mode 100644 index 0000000..33a1122 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfo.kt @@ -0,0 +1,34 @@ +/* Copyright 2022 Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.instanceinfo + +data class InstanceInfo( + val maxChars: Int, + val pollMaxOptions: Int, + val pollMaxLength: Int, + val pollMinDuration: Int, + val pollMaxDuration: Int, + val charactersReservedPerUrl: Int, + val videoSizeLimit: Int, + val imageSizeLimit: Int, + val imageMatrixLimit: Int, + val maxMediaAttachments: Int, + val maxFields: Int, + val maxFieldNameLength: Int?, + val maxFieldValueLength: Int?, + val version: String?, + val translationEnabled: Boolean?, +) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt new file mode 100644 index 0000000..ee2a9a7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/instanceinfo/InstanceInfoRepository.kt @@ -0,0 +1,224 @@ +/* Copyright 2022 Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.instanceinfo + +import android.util.Log +import at.connyduck.calladapter.networkresult.NetworkResult +import at.connyduck.calladapter.networkresult.getOrElse +import at.connyduck.calladapter.networkresult.getOrThrow +import at.connyduck.calladapter.networkresult.map +import at.connyduck.calladapter.networkresult.onSuccess +import at.connyduck.calladapter.networkresult.recoverCatching +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.entity.EmojisEntity +import com.keylesspalace.tusky.db.entity.InstanceInfoEntity +import com.keylesspalace.tusky.di.ApplicationScope +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.Instance +import com.keylesspalace.tusky.entity.InstanceV1 +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.isHttpNotFound +import java.util.concurrent.ConcurrentHashMap +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +@Singleton +class InstanceInfoRepository @Inject constructor( + private val api: MastodonApi, + db: AppDatabase, + private val accountManager: AccountManager, + @ApplicationScope + private val externalScope: CoroutineScope +) { + private val dao = db.instanceDao() + private val instanceName + get() = accountManager.activeAccount!!.domain + + /** In-memory cache for instance data, per instance domain. */ + private var instanceInfoCache = ConcurrentHashMap<String, InstanceInfo>() + + fun precache() { + // We are avoiding some duplicate work but we are not trying too hard. + // We might request it multiple times in parallel which is not a big problem. + // We might also get the results in random order or write them twice but it's also + // not a problem. + // We are just trying to avoid 2 things: + // - fetching it when we already have it + // - caching default value (we want to rather re-fetch if it fails) + if (instanceInfoCache[instanceName] == null) { + externalScope.launch { + fetchAndPersistInstanceInfo().onSuccess { fetched -> + instanceInfoCache[fetched.instance] = fetched.toInfoOrDefault() + } + } + } + } + + val cachedInstanceInfoOrFallback: InstanceInfo + get() = instanceInfoCache[instanceName] ?: null.toInfoOrDefault() + + /** + * Returns the custom emojis of the instance. + * Will always try to fetch them from the api, falls back to cached Emojis in case it is not available. + * Never throws, returns empty list in case of error. + */ + suspend fun getEmojis(): List<Emoji> = withContext(Dispatchers.IO) { + api.getCustomEmojis() + .onSuccess { emojiList -> dao.upsert(EmojisEntity(instanceName, emojiList)) } + .getOrElse { throwable -> + Log.w(TAG, "failed to load custom emojis, falling back to cache", throwable) + dao.getEmojiInfo(instanceName)?.emojiList.orEmpty() + } + } + + /** + * Returns information about the instance. + * Will always try to fetch the most up-to-date data from the api, falls back to cache in case it is not available. + * Never throws, returns defaults of vanilla Mastodon in case of error. + */ + suspend fun getUpdatedInstanceInfoOrFallback(): InstanceInfo = + withContext(Dispatchers.IO) { + fetchAndPersistInstanceInfo() + .getOrElse { throwable -> + Log.w( + TAG, + "failed to load instance, falling back to cache and default values", + throwable + ) + dao.getInstanceInfo(instanceName) + } + }.toInfoOrDefault() + + private suspend fun InstanceInfoRepository.fetchAndPersistInstanceInfo(): NetworkResult<InstanceInfoEntity> = + fetchRemoteInstanceInfo() + .onSuccess { instanceInfoEntity -> + dao.upsert(instanceInfoEntity) + } + + private suspend fun fetchRemoteInstanceInfo(): NetworkResult<InstanceInfoEntity> { + val instance = this.instanceName + return api.getInstance() + .map { it.toEntity() } + .recoverCatching { t -> + if (t.isHttpNotFound()) { + api.getInstanceV1().map { it.toEntity(instance) }.getOrThrow() + } else { + throw t + } + } + } + + private fun InstanceInfoEntity?.toInfoOrDefault() = InstanceInfo( + maxChars = this?.maximumTootCharacters ?: DEFAULT_CHARACTER_LIMIT, + pollMaxOptions = this?.maxPollOptions ?: DEFAULT_MAX_OPTION_COUNT, + pollMaxLength = this?.maxPollOptionLength ?: DEFAULT_MAX_OPTION_LENGTH, + pollMinDuration = this?.minPollDuration ?: DEFAULT_MIN_POLL_DURATION, + pollMaxDuration = this?.maxPollDuration ?: DEFAULT_MAX_POLL_DURATION, + charactersReservedPerUrl = this?.charactersReservedPerUrl + ?: DEFAULT_CHARACTERS_RESERVED_PER_URL, + videoSizeLimit = this?.videoSizeLimit ?: DEFAULT_VIDEO_SIZE_LIMIT, + imageSizeLimit = this?.imageSizeLimit ?: DEFAULT_IMAGE_SIZE_LIMIT, + imageMatrixLimit = this?.imageMatrixLimit ?: DEFAULT_IMAGE_MATRIX_LIMIT, + maxMediaAttachments = this?.maxMediaAttachments + ?: DEFAULT_MAX_MEDIA_ATTACHMENTS, + maxFields = this?.maxFields ?: DEFAULT_MAX_ACCOUNT_FIELDS, + maxFieldNameLength = this?.maxFieldNameLength, + maxFieldValueLength = this?.maxFieldValueLength, + version = this?.version, + translationEnabled = this?.translationEnabled + ) + + private fun Instance.toEntity() = InstanceInfoEntity( + instance = domain, + maximumTootCharacters = this.configuration?.statuses?.maxCharacters + ?: DEFAULT_CHARACTER_LIMIT, + maxPollOptions = this.configuration?.polls?.maxOptions ?: DEFAULT_MAX_OPTION_COUNT, + maxPollOptionLength = this.configuration?.polls?.maxCharactersPerOption + ?: DEFAULT_MAX_OPTION_LENGTH, + minPollDuration = this.configuration?.polls?.minExpirationSeconds + ?: DEFAULT_MIN_POLL_DURATION, + maxPollDuration = this.configuration?.polls?.maxExpirationSeconds + ?: DEFAULT_MAX_POLL_DURATION, + charactersReservedPerUrl = this.configuration?.statuses?.charactersReservedPerUrl + ?: DEFAULT_CHARACTERS_RESERVED_PER_URL, + version = this.version, + videoSizeLimit = this.configuration?.mediaAttachments?.videoSizeLimitBytes?.toInt() + ?: DEFAULT_VIDEO_SIZE_LIMIT, + imageSizeLimit = this.configuration?.mediaAttachments?.imageSizeLimitBytes?.toInt() + ?: DEFAULT_IMAGE_SIZE_LIMIT, + imageMatrixLimit = this.configuration?.mediaAttachments?.imagePixelCountLimit?.toInt() + ?: DEFAULT_IMAGE_MATRIX_LIMIT, + maxMediaAttachments = this.configuration?.statuses?.maxMediaAttachments + ?: DEFAULT_MAX_MEDIA_ATTACHMENTS, + maxFields = this.pleroma?.metadata?.fieldLimits?.maxFields, + maxFieldNameLength = this.pleroma?.metadata?.fieldLimits?.nameLength, + maxFieldValueLength = this.pleroma?.metadata?.fieldLimits?.valueLength, + translationEnabled = this.configuration?.translation?.enabled + ) + + private fun InstanceV1.toEntity(instanceName: String) = + InstanceInfoEntity( + instance = instanceName, + maximumTootCharacters = this.configuration?.statuses?.maxCharacters + ?: this.maxTootChars, + maxPollOptions = this.configuration?.polls?.maxOptions + ?: this.pollConfiguration?.maxOptions, + maxPollOptionLength = this.configuration?.polls?.maxCharactersPerOption + ?: this.pollConfiguration?.maxOptionChars, + minPollDuration = this.configuration?.polls?.minExpiration + ?: this.pollConfiguration?.minExpiration, + maxPollDuration = this.configuration?.polls?.maxExpiration + ?: this.pollConfiguration?.maxExpiration, + charactersReservedPerUrl = this.configuration?.statuses?.charactersReservedPerUrl, + version = this.version, + videoSizeLimit = this.configuration?.mediaAttachments?.videoSizeLimit + ?: this.uploadLimit, + imageSizeLimit = this.configuration?.mediaAttachments?.imageSizeLimit + ?: this.uploadLimit, + imageMatrixLimit = this.configuration?.mediaAttachments?.imageMatrixLimit, + maxMediaAttachments = this.configuration?.statuses?.maxMediaAttachments + ?: this.maxMediaAttachments, + maxFields = this.pleroma?.metadata?.fieldLimits?.maxFields, + maxFieldNameLength = this.pleroma?.metadata?.fieldLimits?.nameLength, + maxFieldValueLength = this.pleroma?.metadata?.fieldLimits?.valueLength, + translationEnabled = null, + ) + + companion object { + private const val TAG = "InstanceInfoRepo" + + const val DEFAULT_CHARACTER_LIMIT = 500 + private const val DEFAULT_MAX_OPTION_COUNT = 4 + private const val DEFAULT_MAX_OPTION_LENGTH = 50 + private const val DEFAULT_MIN_POLL_DURATION = 300 + private const val DEFAULT_MAX_POLL_DURATION = 604800 + + private const val DEFAULT_VIDEO_SIZE_LIMIT = 41943040 // 40MiB + private const val DEFAULT_IMAGE_SIZE_LIMIT = 10485760 // 10MiB + private const val DEFAULT_IMAGE_MATRIX_LIMIT = 16777216 // 4096^2 Pixels + + // Mastodon only counts URLs as this long in terms of status character limits + const val DEFAULT_CHARACTERS_RESERVED_PER_URL = 23 + + const val DEFAULT_MAX_MEDIA_ATTACHMENTS = 4 + const val DEFAULT_MAX_ACCOUNT_FIELDS = 4 + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt new file mode 100644 index 0000000..da28285 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginActivity.kt @@ -0,0 +1,372 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.login + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.text.method.LinkMovementMethod +import android.util.Log +import android.view.Menu +import android.view.View +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope +import at.connyduck.calladapter.networkresult.fold +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.MainActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ActivityLoginBinding +import com.keylesspalace.tusky.entity.AccessToken +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.getNonNullString +import com.keylesspalace.tusky.util.openLinkInCustomTab +import com.keylesspalace.tusky.util.rickRoll +import com.keylesspalace.tusky.util.shouldRickRoll +import com.keylesspalace.tusky.util.viewBinding +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.launch +import okhttp3.HttpUrl + +/** Main login page, the first thing that users see. Has prompt for instance and login button. */ +@AndroidEntryPoint +class LoginActivity : BaseActivity() { + + @Inject + lateinit var mastodonApi: MastodonApi + + private val binding by viewBinding(ActivityLoginBinding::inflate) + + private val oauthRedirectUri: String + get() { + val scheme = getString(R.string.oauth_scheme) + val host = BuildConfig.APPLICATION_ID + return "$scheme://$host/" + } + + private val doWebViewAuth = registerForActivityResult(OauthLogin()) { result -> + when (result) { + is LoginResult.Ok -> lifecycleScope.launch { + fetchOauthToken(result.code) + } + is LoginResult.Err -> displayError(result.errorMessage) + is LoginResult.Cancel -> setLoading(false) + } + } + + private var domain: String = "" + private var clientId: String = "" + private var clientSecret: String = "" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(binding.root) + + if (savedInstanceState == null && + BuildConfig.CUSTOM_INSTANCE.isNotBlank() && + !isAdditionalLogin() && !isAccountMigration() + ) { + binding.domainEditText.setText(BuildConfig.CUSTOM_INSTANCE) + binding.domainEditText.setSelection(BuildConfig.CUSTOM_INSTANCE.length) + } + + if (savedInstanceState != null) { + domain = savedInstanceState.getString(DOMAIN, "") + clientId = savedInstanceState.getString(CLIENT_ID, "") + clientSecret = savedInstanceState.getString(CLIENT_SECRET, "") + } + + if (isAccountMigration()) { + binding.domainEditText.setText(accountManager.activeAccount!!.domain) + binding.domainEditText.isEnabled = false + } + + if (BuildConfig.CUSTOM_LOGO_URL.isNotBlank()) { + Glide.with(binding.loginLogo) + .load(BuildConfig.CUSTOM_LOGO_URL) + .placeholder(null) + .into(binding.loginLogo) + } + + binding.loginButton.setOnClickListener { onLoginClick(true) } + + binding.whatsAnInstanceTextView.setOnClickListener { + val dialog = AlertDialog.Builder(this) + .setMessage(R.string.dialog_whats_an_instance) + .setPositiveButton(R.string.action_close, null) + .show() + val textView = dialog.findViewById<TextView>(android.R.id.message) + textView?.movementMethod = LinkMovementMethod.getInstance() + } + + setSupportActionBar(binding.toolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(isAdditionalLogin() || isAccountMigration()) + supportActionBar?.setDisplayShowTitleEnabled(false) + } + + override fun requiresLogin(): Boolean { + return false + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + menu?.add(R.string.action_browser_login)?.apply { + setOnMenuItemClickListener { + onLoginClick(false) + true + } + } + + return super.onCreateOptionsMenu(menu) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(DOMAIN, domain) + outState.putString(CLIENT_ID, clientId) + outState.putString(CLIENT_SECRET, clientSecret) + } + + private fun onLoginClick(openInWebView: Boolean) { + binding.loginButton.isEnabled = false + binding.domainTextInputLayout.error = null + + domain = canonicalizeDomain(binding.domainEditText.text.toString()) + + try { + HttpUrl.Builder().host(domain).scheme("https").build() + } catch (e: IllegalArgumentException) { + setLoading(false) + binding.domainTextInputLayout.error = getString(R.string.error_invalid_domain) + return + } + + if (shouldRickRoll(this, domain)) { + rickRoll(this) + return + } + + setLoading(true) + + lifecycleScope.launch { + mastodonApi.authenticateApp( + domain, + getString(R.string.app_name), + oauthRedirectUri, + OAUTH_SCOPES, + getString(R.string.tusky_website) + ).fold( + { credentials -> + // Save credentials. These will be put into the savedInstanceState so they get restored after activity recreation. + clientId = credentials.clientId + clientSecret = credentials.clientSecret + + redirectUserToAuthorizeAndLogin(domain, credentials.clientId, openInWebView) + }, + { e -> + binding.loginButton.isEnabled = true + binding.domainTextInputLayout.error = + getString(R.string.error_failed_app_registration) + setLoading(false) + Log.e(TAG, Log.getStackTraceString(e)) + return@launch + } + ) + } + } + + private fun redirectUserToAuthorizeAndLogin( + domain: String, + clientId: String, + openInWebView: Boolean + ) { + // To authorize this app and log in it's necessary to redirect to the domain given, + // login there, and the server will redirect back to the app with its response. + val uri = Uri.Builder() + .scheme("https") + .authority(domain) + .path(MastodonApi.ENDPOINT_AUTHORIZE) + .appendQueryParameter("client_id", clientId) + .appendQueryParameter("redirect_uri", oauthRedirectUri) + .appendQueryParameter("response_type", "code") + .appendQueryParameter("scope", OAUTH_SCOPES) + .build() + + if (openInWebView) { + doWebViewAuth.launch(LoginData(domain, uri, oauthRedirectUri.toUri())) + } else { + openLinkInCustomTab(uri, this) + } + } + + override fun onStart() { + super.onStart() + + /* Check if we are resuming during authorization by seeing if the intent contains the + * redirect that was given to the server. If so, its response is here! */ + val uri = intent.data + + if (uri?.toString()?.startsWith(oauthRedirectUri) == true) { + // This should either have returned an authorization code or an error. + val code = uri.getQueryParameter("code") + val error = uri.getQueryParameter("error") + + /* restore variables from SharedPreferences */ + val domain = preferences.getNonNullString(DOMAIN, "") + val clientId = preferences.getNonNullString(CLIENT_ID, "") + val clientSecret = preferences.getNonNullString(CLIENT_SECRET, "") + + if (code != null && domain.isNotEmpty() && clientId.isNotEmpty() && clientSecret.isNotEmpty()) { + lifecycleScope.launch { + fetchOauthToken(code) + } + } else { + displayError(error) + } + } else { + // first show or user cancelled login + setLoading(false) + } + } + + private fun displayError(error: String?) { + // Authorization failed. Put the error response where the user can read it and they + // can try again. + setLoading(false) + + binding.domainTextInputLayout.error = if (error == null) { + // This case means a junk response was received somehow. + getString(R.string.error_authorization_unknown) + } else { + // Use error returned by the server or fall back to the generic message + Log.e(TAG, "%s %s".format(getString(R.string.error_authorization_denied), error)) + error.ifBlank { getString(R.string.error_authorization_denied) } + } + } + + private suspend fun fetchOauthToken(code: String) { + setLoading(true) + + mastodonApi.fetchOAuthToken( + domain, + clientId, + clientSecret, + oauthRedirectUri, + code, + "authorization_code" + ).fold( + { accessToken -> + fetchAccountDetails(accessToken, domain, clientId, clientSecret) + }, + { e -> + setLoading(false) + binding.domainTextInputLayout.error = + getString(R.string.error_retrieving_oauth_token) + Log.e(TAG, getString(R.string.error_retrieving_oauth_token), e) + } + ) + } + + private suspend fun fetchAccountDetails( + accessToken: AccessToken, + domain: String, + clientId: String, + clientSecret: String + ) { + mastodonApi.accountVerifyCredentials( + domain = domain, + auth = "Bearer ${accessToken.accessToken}" + ).fold({ newAccount -> + accountManager.addAccount( + accessToken = accessToken.accessToken, + domain = domain, + clientId = clientId, + clientSecret = clientSecret, + oauthScopes = OAUTH_SCOPES, + newAccount = newAccount + ) + + val intent = Intent(this, MainActivity::class.java) + intent.putExtra(MainActivity.OPEN_WITH_EXPLODE_ANIMATION, true) + startActivity(intent) + finishAffinity() + }, { e -> + setLoading(false) + binding.domainTextInputLayout.error = + getString(R.string.error_loading_account_details) + Log.e(TAG, getString(R.string.error_loading_account_details), e) + }) + } + + private fun setLoading(loadingState: Boolean) { + if (loadingState) { + binding.loginLoadingLayout.visibility = View.VISIBLE + binding.loginInputLayout.visibility = View.GONE + } else { + binding.loginLoadingLayout.visibility = View.GONE + binding.loginInputLayout.visibility = View.VISIBLE + binding.loginButton.isEnabled = true + } + } + + private fun isAdditionalLogin(): Boolean { + return intent.getIntExtra(LOGIN_MODE, MODE_DEFAULT) == MODE_ADDITIONAL_LOGIN + } + + private fun isAccountMigration(): Boolean { + return intent.getIntExtra(LOGIN_MODE, MODE_DEFAULT) == MODE_MIGRATION + } + + companion object { + private const val TAG = "LoginActivity" // logging tag + private const val OAUTH_SCOPES = "read write follow push" + private const val LOGIN_MODE = "LOGIN_MODE" + private const val DOMAIN = "domain" + private const val CLIENT_ID = "clientId" + private const val CLIENT_SECRET = "clientSecret" + + const val MODE_DEFAULT = 0 + const val MODE_ADDITIONAL_LOGIN = 1 + + // "Migration" is used to update the OAuth scope granted to the client + const val MODE_MIGRATION = 2 + + @JvmStatic + fun getIntent(context: Context, mode: Int): Intent { + val loginIntent = Intent(context, LoginActivity::class.java) + loginIntent.putExtra(LOGIN_MODE, mode) + return loginIntent + } + + /** Make sure the user-entered text is just a fully-qualified domain name. */ + private fun canonicalizeDomain(domain: String): String { + // Strip any schemes out. + var s = domain.replaceFirst("http://", "") + s = s.replaceFirst("https://", "") + // If a username was included (e.g. username@example.com), just take what's after the '@'. + val at = s.lastIndexOf('@') + if (at != -1) { + s = s.substring(at + 1) + } + return s.trim { it <= ' ' } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt new file mode 100644 index 0000000..c2ea86b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewActivity.kt @@ -0,0 +1,231 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.login + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.net.Uri +import android.os.Bundle +import android.os.Parcelable +import android.util.Log +import android.webkit.CookieManager +import android.webkit.WebResourceError +import android.webkit.WebResourceRequest +import android.webkit.WebStorage +import android.webkit.WebView +import android.webkit.WebViewClient +import androidx.activity.result.contract.ActivityResultContract +import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog +import androidx.core.net.toUri +import androidx.lifecycle.lifecycleScope +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ActivityLoginWebviewBinding +import com.keylesspalace.tusky.util.getParcelableExtraCompat +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize + +/** Contract for starting [LoginWebViewActivity]. */ +class OauthLogin : ActivityResultContract<LoginData, LoginResult>() { + override fun createIntent(context: Context, input: LoginData): Intent { + val intent = Intent(context, LoginWebViewActivity::class.java) + intent.putExtra(DATA_EXTRA, input) + return intent + } + + override fun parseResult(resultCode: Int, intent: Intent?): LoginResult { + // Can happen automatically on up or back press + return if (resultCode == Activity.RESULT_CANCELED) { + LoginResult.Cancel + } else { + intent?.getParcelableExtraCompat(RESULT_EXTRA) + ?: LoginResult.Err("failed parsing LoginWebViewActivity result") + } + } + + companion object { + private const val RESULT_EXTRA = "result" + private const val DATA_EXTRA = "data" + + fun parseData(intent: Intent): LoginData { + return intent.getParcelableExtraCompat(DATA_EXTRA)!! + } + + fun makeResultIntent(result: LoginResult): Intent { + val intent = Intent() + intent.putExtra(RESULT_EXTRA, result) + return intent + } + } +} + +@Parcelize +data class LoginData( + val domain: String, + val url: Uri, + val oauthRedirectUrl: Uri +) : Parcelable + +sealed interface LoginResult : Parcelable { + @Parcelize + data class Ok(val code: String) : LoginResult + + @Parcelize + data class Err(val errorMessage: String) : LoginResult + + @Parcelize + data object Cancel : LoginResult +} + +/** Activity to do Oauth process using WebView. */ +@AndroidEntryPoint +class LoginWebViewActivity : BaseActivity() { + private val binding by viewBinding(ActivityLoginWebviewBinding::inflate) + + private val viewModel: LoginWebViewViewModel by viewModels() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + if (BuildConfig.DEBUG) { + WebView.setWebContentsDebuggingEnabled(true) + } + + val data = OauthLogin.parseData(intent) + + setContentView(binding.root) + + setSupportActionBar(binding.loginToolbar) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setDisplayShowTitleEnabled(true) + + setTitle(R.string.title_login) + + val webView = binding.loginWebView + webView.settings.allowContentAccess = false + webView.settings.allowFileAccess = false + webView.settings.databaseEnabled = false + webView.settings.displayZoomControls = false + webView.settings.javaScriptCanOpenWindowsAutomatically = false + // JavaScript needs to be enabled because otherwise 2FA does not work in some instances + @SuppressLint("SetJavaScriptEnabled") + webView.settings.javaScriptEnabled = true + webView.settings.userAgentString += " Tusky/${BuildConfig.VERSION_NAME}" + + val oauthUrl = data.oauthRedirectUrl + + webView.webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView?, url: String?) { + binding.loginProgress.hide() + } + + override fun onReceivedError( + view: WebView, + request: WebResourceRequest, + error: WebResourceError + ) { + Log.d("LoginWeb", "Failed to load ${data.url}: $error") + sendResult(LoginResult.Err(getString(R.string.error_could_not_load_login_page))) + } + + override fun shouldOverrideUrlLoading( + view: WebView, + request: WebResourceRequest + ): Boolean { + return shouldOverrideUrlLoading(request.url) + } + + /* overriding this deprecated method is necessary for it to work on api levels < 24 */ + @Suppress("OVERRIDE_DEPRECATION") + override fun shouldOverrideUrlLoading(view: WebView?, urlString: String?): Boolean { + val url = urlString?.toUri() ?: return false + return shouldOverrideUrlLoading(url) + } + + fun shouldOverrideUrlLoading(url: Uri): Boolean { + return if (url.scheme == oauthUrl.scheme && url.host == oauthUrl.host) { + val error = url.getQueryParameter("error") + if (error != null) { + sendResult(LoginResult.Err(error)) + } else { + val code = url.getQueryParameter("code").orEmpty() + sendResult(LoginResult.Ok(code)) + } + true + } else { + false + } + } + } + + webView.setBackgroundColor(Color.TRANSPARENT) + + if (savedInstanceState == null) { + webView.loadUrl(data.url.toString()) + } else { + webView.restoreState(savedInstanceState) + } + + binding.loginRules.text = getString(R.string.instance_rule_info, data.domain) + + viewModel.init(data.domain) + + lifecycleScope.launch { + viewModel.instanceRules.collect { instanceRules -> + binding.loginRules.visible(instanceRules.isNotEmpty()) + binding.loginRules.setOnClickListener { + AlertDialog.Builder(this@LoginWebViewActivity) + .setTitle(getString(R.string.instance_rule_title, data.domain)) + .setMessage( + instanceRules.joinToString(separator = "\n\n") { "• $it" } + ) + .setPositiveButton(android.R.string.ok, null) + .show() + } + } + } + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + binding.loginWebView.saveState(outState) + } + + override fun onDestroy() { + if (isFinishing) { + // We don't want to keep user session in WebView, we just want our own accessToken + WebStorage.getInstance().deleteAllData() + CookieManager.getInstance().removeAllCookies(null) + } + super.onDestroy() + } + + override fun requiresLogin() = false + + private fun sendResult(result: LoginResult) { + setResult(Activity.RESULT_OK, OauthLogin.makeResultIntent(result)) + finish() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewViewModel.kt new file mode 100644 index 0000000..7ce8cfd --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/login/LoginWebViewViewModel.kt @@ -0,0 +1,74 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.login + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.isHttpNotFound +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +@HiltViewModel +class LoginWebViewViewModel @Inject constructor( + private val api: MastodonApi +) : ViewModel() { + + private val _instanceRules = MutableStateFlow(emptyList<String>()) + val instanceRules = _instanceRules.asStateFlow() + + private var domain: String? = null + + fun init(domain: String) { + if (this.domain == null) { + this.domain = domain + viewModelScope.launch { + api.getInstance(domain).fold( + { instance -> + _instanceRules.value = instance.rules.map { rule -> rule.text } + }, + { throwable -> + if (throwable.isHttpNotFound()) { + api.getInstanceV1(domain).fold( + { instance -> + _instanceRules.value = instance.rules.map { rule -> rule.text } + }, + { throwable2 -> + Log.w( + "LoginWebViewViewModel", + "failed to load instance info", + throwable2 + ) + } + ) + } else { + Log.w( + "LoginWebViewViewModel", + "failed to load instance info", + throwable + ) + } + } + ) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/FollowViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/FollowViewHolder.kt new file mode 100644 index 0000000..dbdc964 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/FollowViewHolder.kt @@ -0,0 +1,69 @@ +/* Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.notifications + +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemFollowBinding +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.unicodeWrap +import com.keylesspalace.tusky.viewdata.NotificationViewData + +class FollowViewHolder( + private val binding: ItemFollowBinding, + private val listener: AccountActionListener, +) : RecyclerView.ViewHolder(binding.root), NotificationsViewHolder { + + override fun bind( + viewData: NotificationViewData.Concrete, + payloads: List<*>, + statusDisplayOptions: StatusDisplayOptions + ) { + val context = itemView.context + val account = viewData.account + val messageTemplate = + context.getString(if (viewData.type == Notification.Type.SIGN_UP) R.string.notification_sign_up_format else R.string.notification_follow_format) + val wrappedDisplayName = account.name.unicodeWrap() + + binding.notificationText.text = messageTemplate.format(wrappedDisplayName) + .emojify(account.emojis, binding.notificationText, statusDisplayOptions.animateEmojis) + + binding.notificationUsername.text = context.getString(R.string.post_username_format, viewData.account.username) + + val emojifiedDisplayName = wrappedDisplayName.emojify( + account.emojis, + binding.notificationDisplayName, + statusDisplayOptions.animateEmojis + ) + binding.notificationDisplayName.text = emojifiedDisplayName + + val avatarRadius = context.resources + .getDimensionPixelSize(R.dimen.avatar_radius_42dp) + loadAvatar( + account.avatar, + binding.notificationAvatar, + avatarRadius, + statusDisplayOptions.animateAvatars, + null + ) + + itemView.setOnClickListener { listener.onViewAccount(account.id) } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationTypeMappers.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationTypeMappers.kt new file mode 100644 index 0000000..d3248a6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationTypeMappers.kt @@ -0,0 +1,104 @@ +/* Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.notifications + +import com.keylesspalace.tusky.components.timeline.Placeholder +import com.keylesspalace.tusky.components.timeline.toAccount +import com.keylesspalace.tusky.components.timeline.toStatus +import com.keylesspalace.tusky.db.entity.NotificationDataEntity +import com.keylesspalace.tusky.db.entity.NotificationEntity +import com.keylesspalace.tusky.db.entity.NotificationReportEntity +import com.keylesspalace.tusky.db.entity.TimelineAccountEntity +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.entity.Report +import com.keylesspalace.tusky.viewdata.NotificationViewData +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.keylesspalace.tusky.viewdata.TranslationViewData + +fun Placeholder.toNotificationEntity( + tuskyAccountId: Long +) = NotificationEntity( + id = this.id, + tuskyAccountId = tuskyAccountId, + type = null, + accountId = null, + statusId = null, + reportId = null, + loading = loading +) + +fun Notification.toEntity( + tuskyAccountId: Long +) = NotificationEntity( + tuskyAccountId = tuskyAccountId, + type = type, + id = id, + accountId = account.id, + statusId = status?.id, + reportId = report?.id, + loading = false +) + +fun Report.toEntity( + tuskyAccountId: Long +) = NotificationReportEntity( + tuskyAccountId = tuskyAccountId, + serverId = id, + category = category, + statusIds = statusIds, + createdAt = createdAt, + targetAccountId = targetAccount.id +) + +fun NotificationDataEntity.toViewData( + translation: TranslationViewData? = null +): NotificationViewData { + if (type == null || account == null) { + return NotificationViewData.Placeholder(id = id, isLoading = loading) + } + + return NotificationViewData.Concrete( + id = id, + type = type, + account = account.toAccount(), + statusViewData = if (status != null && statusAccount != null) { + StatusViewData.Concrete( + status = status.toStatus(statusAccount), + isExpanded = this.status.expanded, + isShowingContent = this.status.contentShowing, + isCollapsed = this.status.contentCollapsed, + translation = translation + ) + } else { + null + }, + report = if (report != null && reportTargetAccount != null) { + report.toReport(reportTargetAccount) + } else { + null + } + ) +} + +fun NotificationReportEntity.toReport( + account: TimelineAccountEntity +) = Report( + id = serverId, + category = category, + statusIds = statusIds, + createdAt = createdAt, + targetAccount = account.toAccount() +) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt new file mode 100644 index 0000000..195e15d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsFragment.kt @@ -0,0 +1,548 @@ +/* Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.notifications + +import android.content.DialogInterface +import android.content.SharedPreferences +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.ListView +import android.widget.PopupWindow +import androidx.appcompat.app.AlertDialog +import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.MenuProvider +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.paging.LoadState +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import at.connyduck.calladapter.networkresult.onFailure +import at.connyduck.sparkbutton.helpers.Utils +import com.google.android.material.appbar.AppBarLayout +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper +import com.keylesspalace.tusky.databinding.FragmentTimelineNotificationsBinding +import com.keylesspalace.tusky.databinding.NotificationsFilterBinding +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.fragment.SFragment +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.interfaces.ReselectableFragment +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.StatusProvider +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.openLink +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import com.keylesspalace.tusky.viewdata.NotificationViewData +import com.keylesspalace.tusky.viewdata.TranslationViewData +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlin.time.DurationUnit +import kotlin.time.toDuration +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class NotificationsFragment : + SFragment(R.layout.fragment_timeline_notifications), + SwipeRefreshLayout.OnRefreshListener, + StatusActionListener, + NotificationActionListener, + AccountActionListener, + MenuProvider, + ReselectableFragment { + + @Inject + lateinit var preferences: SharedPreferences + + @Inject + lateinit var eventHub: EventHub + + private val binding by viewBinding(FragmentTimelineNotificationsBinding::bind) + + private val viewModel: NotificationsViewModel by viewModels() + + private var adapter: NotificationsPagingAdapter? = null + + private var showNotificationsFilterBar: Boolean = true + private var readingOrder: ReadingOrder = ReadingOrder.NEWEST_FIRST + + /** see [com.keylesspalace.tusky.components.timeline.TimelineFragment] for explanation of the load more mechanism */ + private var loadMorePosition: Int? = null + private var statusIdBelowLoadMore: String? = null + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) + + val statusDisplayOptions = StatusDisplayOptions( + animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled, + useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false), + showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true), + useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true), + cardViewMode = if (preferences.getBoolean(PrefKeys.SHOW_CARDS_IN_TIMELINES, false)) { + CardViewMode.INDENTED + } else { + CardViewMode.NONE + }, + confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true), + confirmFavourites = preferences.getBoolean(PrefKeys.CONFIRM_FAVOURITES, false), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), + showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false), + showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia, + openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler + ) + + // setup the notifications filter bar + showNotificationsFilterBar = preferences.getBoolean(PrefKeys.SHOW_NOTIFICATIONS_FILTER, true) + updateFilterBarVisibility() + binding.buttonClear.setOnClickListener { confirmClearNotifications() } + binding.buttonFilter.setOnClickListener { showFilterMenu() } + + // Setup the SwipeRefreshLayout. + binding.swipeRefreshLayout.setOnRefreshListener(this) + + // Setup the RecyclerView. + binding.recyclerView.setHasFixedSize(true) + val adapter = NotificationsPagingAdapter( + accountId = accountManager.activeAccount!!.accountId, + statusListener = this, + notificationActionListener = this, + accountActionListener = this, + statusDisplayOptions = statusDisplayOptions + ) + this.adapter = adapter + binding.recyclerView.layoutManager = LinearLayoutManager(context) + binding.recyclerView.setAccessibilityDelegateCompat( + ListStatusAccessibilityDelegate( + binding.recyclerView, + this, + StatusProvider { pos: Int -> + if (pos in 0 until adapter.itemCount) { + val notification = adapter.peek(pos) + // We support replies only for now + if (notification is NotificationViewData.Concrete) { + return@StatusProvider notification.statusViewData + } else { + return@StatusProvider null + } + } else { + null + } + } + ) + ) + + binding.recyclerView.adapter = adapter + + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + + binding.recyclerView.addItemDecoration( + DividerItemDecoration(context, DividerItemDecoration.VERTICAL) + ) + + readingOrder = ReadingOrder.from(preferences.getString(PrefKeys.READING_ORDER, null)) + + adapter.addLoadStateListener { loadState -> + if (loadState.refresh != LoadState.Loading && loadState.source.refresh != LoadState.Loading) { + binding.swipeRefreshLayout.isRefreshing = false + } + + binding.statusView.hide() + binding.progressBar.hide() + + if (adapter.itemCount == 0) { + when (loadState.refresh) { + is LoadState.NotLoading -> { + if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) { + binding.statusView.show() + binding.statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty) + } + } + is LoadState.Error -> { + binding.statusView.show() + binding.statusView.setup((loadState.refresh as LoadState.Error).error) { onRefresh() } + } + is LoadState.Loading -> { + binding.progressBar.show() + } + } + } + } + + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + val firstPos = (binding.recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + if (firstPos == 0 && positionStart == 0 && adapter.itemCount != itemCount) { + binding.recyclerView.post { + if (getView() != null) { + binding.recyclerView.scrollBy( + 0, + Utils.dpToPx(binding.recyclerView.context, -30) + ) + } + } + } + if (readingOrder == ReadingOrder.OLDEST_FIRST) { + updateReadingPositionForOldestFirst(adapter) + } + } + }) + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.notifications.collectLatest { pagingData -> + adapter.submitData(pagingData) + } + } + + viewLifecycleOwner.lifecycleScope.launch { + eventHub.events.collect { event -> + if (event is PreferenceChangedEvent) { + onPreferenceChanged(adapter, event.preferenceKey) + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.RESUMED) { + accountManager.activeAccount?.let { account -> + NotificationHelper.clearNotificationsForAccount(requireContext(), account) + } + + val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false) + while (!useAbsoluteTime) { + adapter.notifyItemRangeChanged( + 0, + adapter.itemCount, + listOf(StatusBaseViewHolder.Key.KEY_CREATED) + ) + delay(1.toDuration(DurationUnit.MINUTES)) + } + } + } + } + + override fun onDestroyView() { + // Clear the adapter to prevent leaking the View + adapter = null + super.onDestroyView() + } + + override fun onReselect() { + if (view != null) { + binding.recyclerView.layoutManager?.scrollToPosition(0) + binding.recyclerView.stopScroll() + } + } + + override fun onRefresh() { + adapter?.refresh() + } + + override fun onViewAccount(id: String) { + super.viewAccount(id) + } + + override fun onMute(mute: Boolean, id: String, position: Int, notifications: Boolean) { + // not needed, muting via the more menu on statuses is handled in SFragment + } + + override fun onBlock(block: Boolean, id: String, position: Int) { + // not needed, blocking via the more menu on statuses is handled in SFragment + } + + override fun onRespondToFollowRequest(accept: Boolean, id: String, position: Int) { + val notification = adapter?.peek(position) ?: return + viewModel.respondToFollowRequest(accept, accountId = id, notificationId = notification.id) + } + + override fun onViewReport(reportId: String?) { + requireContext().openLink( + "https://${accountManager.activeAccount!!.domain}/admin/reports/$reportId" + ) + } + + override fun onViewTag(tag: String) { + super.viewTag(tag) + } + + override fun onReply(position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + super.reply(status.status) + } + + override fun removeItem(position: Int) { + val notification = adapter?.peek(position) ?: return + viewModel.remove(notification.id) + } + + override fun onReblog(reblog: Boolean, position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.reblog(reblog, status) + } + + override val onMoreTranslate: (translate: Boolean, position: Int) -> Unit + get() = { translate: Boolean, position: Int -> + if (translate) { + onTranslate(position) + } else { + onUntranslate(position) + } + } + + private fun onTranslate(position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewLifecycleOwner.lifecycleScope.launch { + viewModel.translate(status) + .onFailure { + Snackbar.make( + requireView(), + getString(R.string.ui_error_translate, it.message), + Snackbar.LENGTH_LONG + ).show() + } + } + } + + override fun onUntranslate(position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.untranslate(status) + } + + override fun onFavourite(favourite: Boolean, position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.favorite(favourite, status) + } + + override fun onBookmark(bookmark: Boolean, position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.bookmark(bookmark, status) + } + + override fun onVoteInPoll(position: Int, choices: List<Int>) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.voteInPoll(choices, status) + } + + override fun clearWarningAction(position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.clearWarning(status) + } + + override fun onMore(view: View, position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + super.more( + status.status, + view, + position, + (status.translation as? TranslationViewData.Loaded)?.data + ) + } + + override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + super.viewMedia(attachmentIndex, AttachmentViewData.list(status), view) + } + + override fun onViewThread(position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull()?.status ?: return + super.viewThread(status.id, status.url) + } + + override fun onOpenReblog(position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + super.openReblog(status.status) + } + + override fun onExpandedChange(expanded: Boolean, position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.changeExpanded(expanded, status) + } + + override fun onContentHiddenChange(isShowing: Boolean, position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.changeContentShowing(isShowing, status) + } + + override fun onLoadMore(position: Int) { + val adapter = this.adapter + val placeholder = adapter?.peek(position)?.asPlaceholderOrNull() ?: return + loadMorePosition = position + statusIdBelowLoadMore = + if (position + 1 < adapter.itemCount) adapter.peek(position + 1)?.id else null + viewModel.loadMore(placeholder.id) + } + + override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.changeContentCollapsed(isCollapsed, status) + } + + private fun confirmClearNotifications() { + AlertDialog.Builder(requireContext()) + .setMessage(R.string.notification_clear_text) + .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> clearNotifications() } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun clearNotifications() { + viewModel.clearNotifications() + } + + private fun showFilterMenu() { + val notificationTypeList = Notification.Type.visibleTypes.map { type -> + getString(type.uiString) + } + + val adapter = ArrayAdapter(requireContext(), android.R.layout.simple_list_item_multiple_choice, notificationTypeList) + val window = PopupWindow(requireContext()) + val menuBinding = NotificationsFilterBinding.inflate(LayoutInflater.from(requireContext()), binding.root as ViewGroup, false) + + menuBinding.buttonApply.setOnClickListener { + val checkedItems = menuBinding.listView.getCheckedItemPositions() + val excludes = Notification.Type.visibleTypes.filterIndexed { index, _ -> + !checkedItems[index, false] + } + window.dismiss() + viewModel.updateNotificationFilters(excludes.toSet()) + } + + menuBinding.listView.setAdapter(adapter) + menuBinding.listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE) + + Notification.Type.visibleTypes.forEachIndexed { index, type -> + menuBinding.listView.setItemChecked(index, !viewModel.excludes.value.contains(type)) + } + + window.setContentView(menuBinding.root) + window.isFocusable = true + window.width = ViewGroup.LayoutParams.WRAP_CONTENT + window.height = ViewGroup.LayoutParams.WRAP_CONTENT + window.showAsDropDown(binding.buttonFilter) + } + + private fun onPreferenceChanged(adapter: NotificationsPagingAdapter, key: String) { + when (key) { + PrefKeys.MEDIA_PREVIEW_ENABLED -> { + val enabled = accountManager.activeAccount!!.mediaPreviewEnabled + val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled + if (enabled != oldMediaPreviewEnabled) { + adapter.mediaPreviewEnabled = enabled + } + } + + PrefKeys.SHOW_NOTIFICATIONS_FILTER -> { + if (view != null) { + showNotificationsFilterBar = preferences.getBoolean(PrefKeys.SHOW_NOTIFICATIONS_FILTER, true) + updateFilterBarVisibility() + } + } + + PrefKeys.READING_ORDER -> { + readingOrder = ReadingOrder.from( + preferences.getString(PrefKeys.READING_ORDER, null) + ) + } + } + } + + private fun updateFilterBarVisibility() { + val params = binding.swipeRefreshLayout.layoutParams as CoordinatorLayout.LayoutParams + if (showNotificationsFilterBar) { + binding.appBarOptions.setExpanded(true, false) + binding.appBarOptions.show() + // Set content behaviour to hide filter on scroll + params.behavior = AppBarLayout.ScrollingViewBehavior() + } else { + binding.appBarOptions.setExpanded(false, false) + binding.appBarOptions.hide() + // Clear behaviour to hide app bar + params.behavior = null + } + } + + private fun updateReadingPositionForOldestFirst(adapter: NotificationsPagingAdapter) { + var position = loadMorePosition ?: return + val notificationIdBelowLoadMore = statusIdBelowLoadMore ?: return + + var notification: NotificationViewData? + while (adapter.peek(position).let { + notification = it + it != null + } + ) { + if (notification?.id == notificationIdBelowLoadMore) { + val lastVisiblePosition = + (binding.recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() + if (position > lastVisiblePosition) { + binding.recyclerView.scrollToPosition(position) + } + break + } + position++ + } + loadMorePosition = null + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.fragment_notifications, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem) = when (menuItem.itemId) { + R.id.action_refresh -> { + binding.swipeRefreshLayout.isRefreshing = true + onRefresh() + true + } + R.id.action_edit_notification_filter -> { + showFilterMenu() + true + } + R.id.action_clear_notifications -> { + confirmClearNotifications() + true + } + else -> false + } + + companion object { + fun newInstance() = NotificationsFragment() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt new file mode 100644 index 0000000..c3d00c1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsPagingAdapter.kt @@ -0,0 +1,196 @@ +/* Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.notifications + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.adapter.FollowRequestViewHolder +import com.keylesspalace.tusky.adapter.PlaceholderViewHolder +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.databinding.ItemFollowBinding +import com.keylesspalace.tusky.databinding.ItemFollowRequestBinding +import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding +import com.keylesspalace.tusky.databinding.ItemStatusBinding +import com.keylesspalace.tusky.databinding.ItemStatusNotificationBinding +import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding +import com.keylesspalace.tusky.databinding.ItemUnknownNotificationBinding +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.AbsoluteTimeFormatter +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.NotificationViewData + +interface NotificationActionListener { + fun onViewReport(reportId: String?) +} + +interface NotificationsViewHolder { + fun bind( + viewData: NotificationViewData.Concrete, + payloads: List<*>, + statusDisplayOptions: StatusDisplayOptions + ) +} + +class NotificationsPagingAdapter( + private val accountId: String, + private var statusDisplayOptions: StatusDisplayOptions, + private val statusListener: StatusActionListener, + private val notificationActionListener: NotificationActionListener, + private val accountActionListener: AccountActionListener +) : PagingDataAdapter<NotificationViewData, RecyclerView.ViewHolder>(NotificationsDifferCallback) { + + var mediaPreviewEnabled: Boolean + get() = statusDisplayOptions.mediaPreviewEnabled + set(mediaPreviewEnabled) { + statusDisplayOptions = statusDisplayOptions.copy( + mediaPreviewEnabled = mediaPreviewEnabled + ) + notifyItemRangeChanged(0, itemCount) + } + + private val absoluteTimeFormatter = AbsoluteTimeFormatter() + + init { + stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY + } + + override fun getItemViewType(position: Int): Int { + return when (val notification = getItem(position)) { + is NotificationViewData.Concrete -> { + when (notification.type) { + Notification.Type.MENTION, + Notification.Type.POLL -> VIEW_TYPE_STATUS + Notification.Type.STATUS, + Notification.Type.FAVOURITE, + Notification.Type.REBLOG, + Notification.Type.UPDATE -> VIEW_TYPE_STATUS_NOTIFICATION + Notification.Type.FOLLOW, + Notification.Type.SIGN_UP -> VIEW_TYPE_FOLLOW + Notification.Type.FOLLOW_REQUEST -> VIEW_TYPE_FOLLOW_REQUEST + Notification.Type.REPORT -> VIEW_TYPE_REPORT + else -> VIEW_TYPE_UNKNOWN + } + } + else -> VIEW_TYPE_PLACEHOLDER + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + VIEW_TYPE_STATUS -> StatusViewHolder( + ItemStatusBinding.inflate(inflater, parent, false), + statusListener, + accountId + ) + VIEW_TYPE_STATUS_NOTIFICATION -> StatusNotificationViewHolder( + ItemStatusNotificationBinding.inflate(inflater, parent, false), + statusListener, + absoluteTimeFormatter + ) + VIEW_TYPE_FOLLOW -> FollowViewHolder( + ItemFollowBinding.inflate(inflater, parent, false), + accountActionListener + ) + VIEW_TYPE_FOLLOW_REQUEST -> FollowRequestViewHolder( + ItemFollowRequestBinding.inflate(inflater, parent, false), + accountActionListener, + statusListener, + true + ) + VIEW_TYPE_PLACEHOLDER -> PlaceholderViewHolder( + ItemStatusPlaceholderBinding.inflate(inflater, parent, false), + statusListener + ) + VIEW_TYPE_REPORT -> ReportNotificationViewHolder( + ItemReportNotificationBinding.inflate(inflater, parent, false), + notificationActionListener, + accountActionListener + ) + else -> UnknownNotificationViewHolder( + ItemUnknownNotificationBinding.inflate(inflater, parent, false) + ) + } + } + + override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) { + bindViewHolder(viewHolder, position, emptyList()) + } + + override fun onBindViewHolder( + viewHolder: RecyclerView.ViewHolder, + position: Int, + payloads: List<Any> + ) { + bindViewHolder(viewHolder, position, payloads) + } + + private fun bindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int, payloads: List<Any>) { + getItem(position)?.let { notification -> + when (notification) { + is NotificationViewData.Concrete -> + (viewHolder as NotificationsViewHolder).bind(notification, payloads, statusDisplayOptions) + is NotificationViewData.Placeholder -> { + (viewHolder as PlaceholderViewHolder).setup(notification.isLoading) + } + } + } + } + + companion object { + private const val VIEW_TYPE_STATUS = 0 + private const val VIEW_TYPE_STATUS_NOTIFICATION = 1 + private const val VIEW_TYPE_FOLLOW = 2 + private const val VIEW_TYPE_FOLLOW_REQUEST = 3 + private const val VIEW_TYPE_PLACEHOLDER = 4 + private const val VIEW_TYPE_REPORT = 5 + private const val VIEW_TYPE_UNKNOWN = 6 + + val NotificationsDifferCallback = object : DiffUtil.ItemCallback<NotificationViewData>() { + override fun areItemsTheSame( + oldItem: NotificationViewData, + newItem: NotificationViewData + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: NotificationViewData, + newItem: NotificationViewData + ): Boolean { + return false // Items are different always. It allows to refresh timestamp on every view holder update + } + + override fun getChangePayload( + oldItem: NotificationViewData, + newItem: NotificationViewData + ): Any? { + return if (oldItem == newItem) { + // If items are equal - update timestamp only + listOf(StatusBaseViewHolder.Key.KEY_CREATED) + } else { + // If items are different - update the whole view holder + null + } + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsRemoteMediator.kt new file mode 100644 index 0000000..d6627da --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsRemoteMediator.kt @@ -0,0 +1,208 @@ +/* Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.notifications + +import android.util.Log +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import androidx.room.withTransaction +import com.keylesspalace.tusky.components.timeline.Placeholder +import com.keylesspalace.tusky.components.timeline.toEntity +import com.keylesspalace.tusky.components.timeline.util.ifExpected +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.entity.NotificationDataEntity +import com.keylesspalace.tusky.db.entity.TimelineStatusEntity +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.isLessThan +import retrofit2.HttpException + +@OptIn(ExperimentalPagingApi::class) +class NotificationsRemoteMediator( + private val accountManager: AccountManager, + private val api: MastodonApi, + private val db: AppDatabase, + var excludes: Set<Notification.Type> +) : RemoteMediator<Int, NotificationDataEntity>() { + + private var initialRefresh = false + + private val notificationsDao = db.notificationsDao() + private val accountDao = db.timelineAccountDao() + private val statusDao = db.timelineStatusDao() + private val activeAccount = accountManager.activeAccount!! + + override suspend fun load( + loadType: LoadType, + state: PagingState<Int, NotificationDataEntity> + ): MediatorResult { + if (!activeAccount.isLoggedIn()) { + return MediatorResult.Success(endOfPaginationReached = true) + } + + try { + var dbEmpty = false + + val topPlaceholderId = if (loadType == LoadType.REFRESH) { + notificationsDao.getTopPlaceholderId(activeAccount.id) + } else { + null // don't execute the query if it is not needed + } + + if (!initialRefresh && loadType == LoadType.REFRESH) { + val topId = notificationsDao.getTopId(activeAccount.id) + topId?.let { cachedTopId -> + val notificationResponse = api.notifications( + maxId = cachedTopId, + // so already existing placeholders don't get accidentally overwritten + sinceId = topPlaceholderId, + limit = state.config.pageSize, + excludes = excludes + ) + + val notifications = notificationResponse.body() + if (notificationResponse.isSuccessful && notifications != null) { + db.withTransaction { + replaceNotificationRange(notifications, state) + } + } + } + initialRefresh = true + dbEmpty = topId == null + } + + val notificationResponse = when (loadType) { + LoadType.REFRESH -> { + api.notifications(sinceId = topPlaceholderId, limit = state.config.pageSize, excludes = excludes) + } + LoadType.PREPEND -> { + return MediatorResult.Success(endOfPaginationReached = true) + } + LoadType.APPEND -> { + val maxId = state.pages.findLast { it.data.isNotEmpty() }?.data?.lastOrNull()?.id + api.notifications(maxId = maxId, limit = state.config.pageSize, excludes = excludes) + } + } + + val notifications = notificationResponse.body() + if (!notificationResponse.isSuccessful || notifications == null) { + return MediatorResult.Error(HttpException(notificationResponse)) + } + + db.withTransaction { + val overlappedNotifications = replaceNotificationRange(notifications, state) + + /* In case we loaded a whole page and there was no overlap with existing statuses, + we insert a placeholder because there might be even more unknown statuses */ + if (loadType == LoadType.REFRESH && overlappedNotifications == 0 && notifications.size == state.config.pageSize && !dbEmpty) { + /* This overrides the last of the newly loaded statuses with a placeholder + to guarantee the placeholder has an id that exists on the server as not all + servers handle client generated ids as expected */ + notificationsDao.insertNotification( + Placeholder(notifications.last().id, loading = false).toNotificationEntity(activeAccount.id) + ) + } + } + return MediatorResult.Success(endOfPaginationReached = notifications.isEmpty()) + } catch (e: Exception) { + return ifExpected(e) { + Log.w(TAG, "Failed to load notifications", e) + MediatorResult.Error(e) + } + } + } + + /** + * Deletes all notifications in a given range and inserts new notifications. + * This is necessary so notifications that have been deleted on the server are cleaned up. + * Should be run in a transaction as it executes multiple db updates + * @param notifications the new notifications + * @return the number of old notifications that have been cleared from the database + */ + private suspend fun replaceNotificationRange(notifications: List<Notification>, state: PagingState<Int, NotificationDataEntity>): Int { + val overlappedNotifications = if (notifications.isNotEmpty()) { + notificationsDao.deleteRange(activeAccount.id, notifications.last().id, notifications.first().id) + } else { + 0 + } + + for (notification in notifications) { + accountDao.insert(notification.account.toEntity(activeAccount.id)) + notification.report?.let { report -> + accountDao.insert(report.targetAccount.toEntity(activeAccount.id)) + notificationsDao.insertReport(report.toEntity(activeAccount.id)) + } + + // check if we already have one of the newly loaded statuses cached locally + // in case we do, copy the local state (expanded, contentShowing, contentCollapsed) over so it doesn't get lost + var oldStatus: TimelineStatusEntity? = null + for (page in state.pages) { + oldStatus = page.data.find { s -> + s.id == notification.id + }?.status + if (oldStatus != null) break + } + + notification.status?.let { status -> + val expanded = oldStatus?.expanded ?: activeAccount.alwaysOpenSpoiler + val contentShowing = oldStatus?.contentShowing ?: (activeAccount.alwaysShowSensitiveMedia || !status.sensitive) + val contentCollapsed = oldStatus?.contentCollapsed ?: true + + accountDao.insert(status.account.toEntity(activeAccount.id)) + + statusDao.insert( + status.toEntity( + tuskyAccountId = activeAccount.id, + expanded = expanded, + contentShowing = contentShowing, + contentCollapsed = contentCollapsed + ) + ) + } + + notificationsDao.insertNotification( + notification.toEntity( + activeAccount.id + ) + ) + } + notifications.firstOrNull()?.let { notification -> + saveNewestNotificationId(notification) + } + return overlappedNotifications + } + + private fun saveNewestNotificationId(notification: Notification) { + val account = accountManager.activeAccount + // make sure the account we are currently working with is still active + if (account == activeAccount) { + val lastNotificationId: String = activeAccount.lastNotificationId + val newestNotificationId = notification.id + if (lastNotificationId.isLessThan(newestNotificationId)) { + Log.d(TAG, "saving newest noti id: $lastNotificationId for account ${account.id}") + account.lastNotificationId = newestNotificationId + accountManager.saveAccount(account) + } + } + } + + companion object { + private const val TAG = "NotificationsRM" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt new file mode 100644 index 0000000..9c18ce9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/NotificationsViewModel.kt @@ -0,0 +1,417 @@ +/* Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.notifications + +import android.content.SharedPreferences +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import androidx.paging.filter +import androidx.paging.map +import androidx.room.withTransaction +import at.connyduck.calladapter.networkresult.NetworkResult +import at.connyduck.calladapter.networkresult.fold +import at.connyduck.calladapter.networkresult.map +import at.connyduck.calladapter.networkresult.onFailure +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.FilterUpdatedEvent +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder +import com.keylesspalace.tusky.components.timeline.Placeholder +import com.keylesspalace.tusky.components.timeline.toEntity +import com.keylesspalace.tusky.components.timeline.util.ifExpected +import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.network.FilterModel +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.usecase.TimelineCases +import com.keylesspalace.tusky.util.EmptyPagingSource +import com.keylesspalace.tusky.util.deserialize +import com.keylesspalace.tusky.util.serialize +import com.keylesspalace.tusky.viewdata.NotificationViewData +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.keylesspalace.tusky.viewdata.TranslationViewData +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import retrofit2.HttpException + +@HiltViewModel +class NotificationsViewModel @Inject constructor( + private val timelineCases: TimelineCases, + private val api: MastodonApi, + eventHub: EventHub, + private val accountManager: AccountManager, + private val preferences: SharedPreferences, + private val filterModel: FilterModel, + private val db: AppDatabase, +) : ViewModel() { + + private val refreshTrigger = MutableStateFlow(0L) + + private val _excludes = MutableStateFlow( + accountManager.activeAccount?.let { account -> deserialize(account.notificationsFilter) } ?: emptySet() + ) + val excludes: StateFlow<Set<Notification.Type>> = _excludes.asStateFlow() + + /** Map from notification id to translation. */ + private val translations = MutableStateFlow(mapOf<String, TranslationViewData>()) + + private var remoteMediator = NotificationsRemoteMediator(accountManager, api, db, excludes.value) + + private var readingOrder: ReadingOrder = + ReadingOrder.from(preferences.getString(PrefKeys.READING_ORDER, null)) + + @OptIn(ExperimentalPagingApi::class, ExperimentalCoroutinesApi::class) + val notifications = refreshTrigger.flatMapLatest { + Pager( + config = PagingConfig( + pageSize = LOAD_AT_ONCE + ), + remoteMediator = remoteMediator, + pagingSourceFactory = { + val activeAccount = accountManager.activeAccount + if (activeAccount == null) { + EmptyPagingSource() + } else { + db.notificationsDao().getNotifications(activeAccount.id) + } + } + ).flow + .cachedIn(viewModelScope) + .combine(translations) { pagingData, translations -> + pagingData.map { notification -> + val translation = translations[notification.status?.serverId] + notification.toViewData(translation = translation) + }.filter { notificationViewData -> + shouldFilterStatus(notificationViewData) != Filter.Action.HIDE + } + } + } + .flowOn(Dispatchers.Default) + + init { + viewModelScope.launch { + eventHub.events.collect { event -> + if (event is PreferenceChangedEvent) { + onPreferenceChanged(event.preferenceKey) + } + if (event is FilterUpdatedEvent && event.filterContext.contains(Filter.Kind.NOTIFICATIONS.kind)) { + refreshTrigger.value += 1 + } + } + } + filterModel.kind = Filter.Kind.NOTIFICATIONS + } + + fun updateNotificationFilters(newFilters: Set<Notification.Type>) { + if (newFilters != _excludes.value) { + val account = accountManager.activeAccount + if (account != null) { + viewModelScope.launch { + account.notificationsFilter = serialize(newFilters) + accountManager.saveAccount(account) + remoteMediator.excludes = newFilters + db.notificationsDao().cleanupNotifications(account.id, 0) + refreshTrigger.value++ + _excludes.value = newFilters + } + } + } + } + + private fun shouldFilterStatus(notificationViewData: NotificationViewData): Filter.Action { + return when ((notificationViewData as? NotificationViewData.Concrete)?.type) { + Notification.Type.MENTION, Notification.Type.STATUS, Notification.Type.POLL -> { + notificationViewData.statusViewData?.let { statusViewData -> + statusViewData.filterAction = filterModel.shouldFilterStatus(statusViewData.actionable) + return statusViewData.filterAction + } + Filter.Action.NONE + } + else -> Filter.Action.NONE + } + } + + fun respondToFollowRequest(accept: Boolean, accountId: String, notificationId: String) { + viewModelScope.launch { + if (accept) { + api.authorizeFollowRequest(accountId) + } else { + api.rejectFollowRequest(accountId) + }.fold( + onSuccess = { + // since the follow request has been responded, the notification can be deleted. The Ui will update automatically. + db.notificationsDao().delete(accountManager.activeAccount!!.id, notificationId) + if (accept) { + // refresh the notifications so the new follow notification will be loaded + refreshTrigger.value++ + } + }, + onFailure = { t -> + Log.e(TAG, "Failed to to respond to follow request from account id $accountId.", t) + } + ) + } + } + + fun reblog(reblog: Boolean, status: StatusViewData.Concrete): Job = viewModelScope.launch { + timelineCases.reblog(status.actionableId, reblog).onFailure { t -> + ifExpected(t) { + Log.w(TAG, "Failed to reblog status " + status.actionableId, t) + } + } + } + + fun favorite(favorite: Boolean, status: StatusViewData.Concrete): Job = viewModelScope.launch { + timelineCases.favourite(status.actionableId, favorite).onFailure { t -> + ifExpected(t) { + Log.d(TAG, "Failed to favourite status " + status.actionableId, t) + } + } + } + + fun bookmark(bookmark: Boolean, status: StatusViewData.Concrete): Job = viewModelScope.launch { + timelineCases.bookmark(status.actionableId, bookmark).onFailure { t -> + ifExpected(t) { + Log.d(TAG, "Failed to bookmark status " + status.actionableId, t) + } + } + } + + fun voteInPoll(choices: List<Int>, status: StatusViewData.Concrete) = viewModelScope.launch { + val poll = status.status.actionableStatus.poll ?: run { + Log.d(TAG, "No poll on status ${status.id}") + return@launch + } + timelineCases.voteInPoll(status.actionableId, poll.id, choices).onFailure { t -> + ifExpected(t) { + Log.d(TAG, "Failed to vote in poll: " + status.actionableId, t) + } + } + } + + fun changeExpanded(expanded: Boolean, status: StatusViewData.Concrete) { + viewModelScope.launch { + db.timelineStatusDao() + .setExpanded(accountManager.activeAccount!!.id, status.id, expanded) + } + } + + fun changeContentShowing(isShowing: Boolean, status: StatusViewData.Concrete) { + viewModelScope.launch { + db.timelineStatusDao() + .setContentShowing(accountManager.activeAccount!!.id, status.id, isShowing) + } + } + + fun changeContentCollapsed(isCollapsed: Boolean, status: StatusViewData.Concrete) { + viewModelScope.launch { + db.timelineStatusDao() + .setContentCollapsed(accountManager.activeAccount!!.id, status.id, isCollapsed) + } + } + + fun remove(notificationId: String) { + viewModelScope.launch { + db.notificationsDao().delete(accountManager.activeAccount!!.id, notificationId) + } + } + + fun clearWarning(status: StatusViewData.Concrete) { + viewModelScope.launch { + db.timelineStatusDao().clearWarning(accountManager.activeAccount!!.id, status.actionableId) + } + } + + fun clearNotifications() { + viewModelScope.launch { + api.clearNotifications().fold( + { + db.notificationsDao().cleanupNotifications(accountManager.activeAccount!!.id, 0) + }, + { t -> + Log.w(TAG, "failed to clear notifications", t) + } + ) + } + } + + suspend fun translate(status: StatusViewData.Concrete): NetworkResult<Unit> { + translations.value += (status.id to TranslationViewData.Loading) + return timelineCases.translate(status.actionableId) + .map { translation -> + translations.value += (status.id to TranslationViewData.Loaded(translation)) + } + .onFailure { + translations.value -= status.id + } + } + + fun untranslate(status: StatusViewData.Concrete) { + translations.value -= status.id + } + + fun loadMore(placeholderId: String) { + viewModelScope.launch { + try { + val notificationsDao = db.notificationsDao() + + val activeAccount = accountManager.activeAccount!! + + notificationsDao.insertNotification( + Placeholder(placeholderId, loading = true).toNotificationEntity( + activeAccount.id + ) + ) + + val response = db.withTransaction { + val idAbovePlaceholder = notificationsDao.getIdAbove(activeAccount.id, placeholderId) + val idBelowPlaceholder = notificationsDao.getIdBelow(activeAccount.id, placeholderId) + when (readingOrder) { + // Using minId, loads up to LOAD_AT_ONCE statuses with IDs immediately + // after minId and no larger than maxId + ReadingOrder.OLDEST_FIRST -> api.notifications( + maxId = idAbovePlaceholder, + minId = idBelowPlaceholder, + limit = TimelineViewModel.LOAD_AT_ONCE, + excludes = excludes.value + ) + // Using sinceId, loads up to LOAD_AT_ONCE statuses immediately before + // maxId, and no smaller than minId. + ReadingOrder.NEWEST_FIRST -> api.notifications( + maxId = idAbovePlaceholder, + sinceId = idBelowPlaceholder, + limit = TimelineViewModel.LOAD_AT_ONCE, + excludes = excludes.value + ) + } + } + + val notifications = response.body() + if (!response.isSuccessful || notifications == null) { + loadMoreFailed(placeholderId, HttpException(response)) + return@launch + } + + val statusDao = db.timelineStatusDao() + val accountDao = db.timelineAccountDao() + + db.withTransaction { + notificationsDao.delete(activeAccount.id, placeholderId) + + val overlappedNotifications = if (notifications.isNotEmpty()) { + notificationsDao.deleteRange( + activeAccount.id, + notifications.last().id, + notifications.first().id + ) + } else { + 0 + } + + for (notification in notifications) { + accountDao.insert(notification.account.toEntity(activeAccount.id)) + notification.report?.let { report -> + accountDao.insert(report.targetAccount.toEntity(activeAccount.id)) + notificationsDao.insertReport(report.toEntity(activeAccount.id)) + } + notification.status?.let { status -> + accountDao.insert(status.account.toEntity(activeAccount.id)) + + statusDao.insert( + status.toEntity( + tuskyAccountId = activeAccount.id, + expanded = activeAccount.alwaysOpenSpoiler, + contentShowing = activeAccount.alwaysShowSensitiveMedia || !status.sensitive, + contentCollapsed = true + ) + ) + } + notificationsDao.insertNotification( + notification.toEntity( + activeAccount.id + ) + ) + } + + /* In case we loaded a whole page and there was no overlap with existing notifications, + we insert a placeholder because there might be even more unknown notifications */ + if (overlappedNotifications == 0 && notifications.size == TimelineViewModel.LOAD_AT_ONCE) { + /* This overrides the first/last of the newly loaded notifications with a placeholder + to guarantee the placeholder has an id that exists on the server as not all + servers handle client generated ids as expected */ + val idToConvert = when (readingOrder) { + ReadingOrder.OLDEST_FIRST -> notifications.first().id + ReadingOrder.NEWEST_FIRST -> notifications.last().id + } + notificationsDao.insertNotification( + Placeholder( + idToConvert, + loading = false + ).toNotificationEntity(activeAccount.id) + ) + } + } + } catch (e: Exception) { + ifExpected(e) { + loadMoreFailed(placeholderId, e) + } + } + } + } + + private suspend fun loadMoreFailed(placeholderId: String, e: Exception) { + Log.w(TAG, "failed loading notifications", e) + val activeAccount = accountManager.activeAccount!! + db.notificationsDao() + .insertNotification( + Placeholder(placeholderId, loading = false).toNotificationEntity(activeAccount.id) + ) + } + + private fun onPreferenceChanged(key: String) { + when (key) { + PrefKeys.READING_ORDER -> { + readingOrder = ReadingOrder.from( + preferences.getString(PrefKeys.READING_ORDER, null) + ) + } + } + } + + companion object { + private const val LOAD_AT_ONCE = 30 + private const val TAG = "NotificationsViewModel" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/ReportNotificationViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/ReportNotificationViewHolder.kt new file mode 100644 index 0000000..f7c0e71 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/ReportNotificationViewHolder.kt @@ -0,0 +1,96 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.notifications + +import android.content.Context +import android.text.TextUtils +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemReportNotificationBinding +import com.keylesspalace.tusky.interfaces.AccountActionListener +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.getRelativeTimeSpanString +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.unicodeWrap +import com.keylesspalace.tusky.util.updateEmojiTargets +import com.keylesspalace.tusky.viewdata.NotificationViewData + +class ReportNotificationViewHolder( + private val binding: ItemReportNotificationBinding, + private val listener: NotificationActionListener, + private val accountActionListener: AccountActionListener +) : RecyclerView.ViewHolder(binding.root), NotificationsViewHolder { + + override fun bind( + viewData: NotificationViewData.Concrete, + payloads: List<*>, + statusDisplayOptions: StatusDisplayOptions + ) { + val report = viewData.report!! + val reporter = viewData.account + + binding.notificationTopText.updateEmojiTargets { + val reporterName = reporter.name.unicodeWrap().emojify(reporter.emojis, statusDisplayOptions.animateEmojis) + val reporteeName = report.targetAccount.name.unicodeWrap().emojify(report.targetAccount.emojis, statusDisplayOptions.animateEmojis) + + // Context.getString() returns a String and doesn't support Spannable. + // Convert the placeholders to the format used by TextUtils.expandTemplate which does. + val topText = + view.context.getString(R.string.notification_header_report_format, "^1", "^2") + view.text = TextUtils.expandTemplate(topText, reporterName, reporteeName) + } + binding.notificationSummary.text = itemView.context.getString(R.string.notification_summary_report_format, getRelativeTimeSpanString(itemView.context, report.createdAt.time, System.currentTimeMillis()), report.statusIds?.size ?: 0) + binding.notificationCategory.text = getTranslatedCategory(itemView.context, report.category) + + loadAvatar( + report.targetAccount.avatar, + binding.notificationReporteeAvatar, + itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_36dp), + statusDisplayOptions.animateAvatars, + ) + loadAvatar( + reporter.avatar, + binding.notificationReporterAvatar, + itemView.context.resources.getDimensionPixelSize(R.dimen.avatar_radius_24dp), + statusDisplayOptions.animateAvatars, + ) + + binding.notificationReporteeAvatar.setOnClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + accountActionListener.onViewAccount(report.targetAccount.id) + } + } + binding.notificationReporterAvatar.setOnClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + accountActionListener.onViewAccount(reporter.id) + } + } + + itemView.setOnClickListener { listener.onViewReport(report.id) } + } + + private fun getTranslatedCategory(context: Context, rawCategory: String): String { + return when (rawCategory) { + "violation" -> context.getString(R.string.report_category_violation) + "spam" -> context.getString(R.string.report_category_spam) + "legal" -> context.getString(R.string.report_category_legal) + "other" -> context.getString(R.string.report_category_other) + else -> rawCategory + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusNotificationViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusNotificationViewHolder.kt new file mode 100644 index 0000000..e1a51c3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusNotificationViewHolder.kt @@ -0,0 +1,363 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.components.notifications + +import android.content.Context +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.text.InputFilter +import android.text.Spanned +import android.text.format.DateUtils +import android.text.style.StyleSpan +import android.view.View +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.text.toSpannable +import androidx.recyclerview.widget.RecyclerView +import at.connyduck.sparkbutton.helpers.Utils +import com.bumptech.glide.Glide +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.databinding.ItemStatusNotificationBinding +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.AbsoluteTimeFormatter +import com.keylesspalace.tusky.util.SmartLengthInputFilter +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.getRelativeTimeSpanString +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.setClickableText +import com.keylesspalace.tusky.util.unicodeWrap +import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.viewdata.NotificationViewData +import com.keylesspalace.tusky.viewdata.StatusViewData +import java.util.Date + +internal class StatusNotificationViewHolder( + private val binding: ItemStatusNotificationBinding, + private val statusActionListener: StatusActionListener, + private val absoluteTimeFormatter: AbsoluteTimeFormatter +) : NotificationsViewHolder, RecyclerView.ViewHolder(binding.root) { + private val avatarRadius48dp = itemView.context.resources.getDimensionPixelSize( + R.dimen.avatar_radius_48dp + ) + private val avatarRadius36dp = itemView.context.resources.getDimensionPixelSize( + R.dimen.avatar_radius_36dp + ) + private val avatarRadius24dp = itemView.context.resources.getDimensionPixelSize( + R.dimen.avatar_radius_24dp + ) + + override fun bind( + viewData: NotificationViewData.Concrete, + payloads: List<*>, + statusDisplayOptions: StatusDisplayOptions + ) { + val statusViewData = viewData.statusViewData + if (payloads.isEmpty()) { + /* in some very rare cases servers sends null status even though they should not */ + if (statusViewData == null) { + showNotificationContent(false) + } else { + showNotificationContent(true) + val (_, _, account, _, _, _, _, createdAt) = statusViewData.actionable + setDisplayName(account.name, account.emojis, statusDisplayOptions.animateEmojis) + setUsername(account.username) + setCreatedAt(createdAt, statusDisplayOptions.useAbsoluteTime) + if (viewData.type == Notification.Type.STATUS || + viewData.type == Notification.Type.UPDATE + ) { + setAvatar( + account.avatar, + account.bot, + statusDisplayOptions.animateAvatars, + statusDisplayOptions.showBotOverlay + ) + } else { + setAvatars( + account.avatar, + viewData.account.avatar, + statusDisplayOptions.animateAvatars + ) + } + + binding.notificationContainer.setOnClickListener { + statusActionListener.onViewThread(bindingAdapterPosition) + } + binding.notificationContent.setOnClickListener { + statusActionListener.onViewThread(bindingAdapterPosition) + } + binding.notificationTopText.setOnClickListener { + statusActionListener.onViewAccount(viewData.account.id) + } + } + setMessage(viewData, statusActionListener, statusDisplayOptions.animateEmojis) + } else { + for (item in payloads) { + if (StatusBaseViewHolder.Key.KEY_CREATED == item && statusViewData != null) { + setCreatedAt( + statusViewData.status.actionableStatus.createdAt, + statusDisplayOptions.useAbsoluteTime + ) + } + } + } + } + + private fun showNotificationContent(show: Boolean) { + binding.statusDisplayName.visible(show) + binding.statusUsername.visible(show) + binding.statusMetaInfo.visible(show) + binding.notificationContentWarningDescription.visible(show) + binding.notificationContentWarningButton.visible(show) + binding.notificationContent.visible(show) + binding.notificationStatusAvatar.visible(show) + binding.notificationNotificationAvatar.visible(show) + } + + private fun setDisplayName(name: String, emojis: List<Emoji>, animateEmojis: Boolean) { + val emojifiedName = name.emojify(emojis, binding.statusDisplayName, animateEmojis) + binding.statusDisplayName.text = emojifiedName + } + + private fun setUsername(name: String) { + val context = binding.statusUsername.context + val format = context.getString(R.string.post_username_format) + val usernameText = String.format(format, name) + binding.statusUsername.text = usernameText + } + + private fun setCreatedAt(createdAt: Date, useAbsoluteTime: Boolean) { + if (useAbsoluteTime) { + binding.statusMetaInfo.text = absoluteTimeFormatter.format(createdAt, true) + } else { + val readout: String // visible timestamp + val readoutAloud: CharSequence // for screenreaders so they don't mispronounce timestamps like "17m" as 17 meters + + val then = createdAt.time + val now = System.currentTimeMillis() + readout = getRelativeTimeSpanString(binding.statusMetaInfo.context, then, now) + readoutAloud = DateUtils.getRelativeTimeSpanString( + then, + now, + DateUtils.SECOND_IN_MILLIS, + DateUtils.FORMAT_ABBREV_RELATIVE + ) + + binding.statusMetaInfo.text = readout + binding.statusMetaInfo.contentDescription = readoutAloud + } + } + + private fun getIconWithColor( + context: Context, + @DrawableRes drawable: Int, + @ColorRes color: Int + ): Drawable? { + val icon = AppCompatResources.getDrawable(context, drawable) + icon?.setTint(context.getColor(color)) + return icon + } + + private fun setAvatar(statusAvatarUrl: String?, isBot: Boolean, animateAvatars: Boolean, showBotOverlay: Boolean) { + binding.notificationStatusAvatar.setPaddingRelative(0, 0, 0, 0) + loadAvatar( + statusAvatarUrl, + binding.notificationStatusAvatar, + avatarRadius48dp, + animateAvatars + ) + if (showBotOverlay && isBot) { + binding.notificationNotificationAvatar.visibility = View.VISIBLE + Glide.with(binding.notificationNotificationAvatar) + .load(R.drawable.bot_badge) + .into(binding.notificationNotificationAvatar) + } else { + binding.notificationNotificationAvatar.visibility = View.GONE + } + } + + private fun setAvatars(statusAvatarUrl: String?, notificationAvatarUrl: String?, animateAvatars: Boolean) { + val padding = Utils.dpToPx(binding.notificationStatusAvatar.context, 12) + binding.notificationStatusAvatar.setPaddingRelative(0, 0, padding, padding) + loadAvatar( + statusAvatarUrl, + binding.notificationStatusAvatar, + avatarRadius36dp, + animateAvatars + ) + binding.notificationNotificationAvatar.visibility = View.VISIBLE + loadAvatar( + notificationAvatarUrl, + binding.notificationNotificationAvatar, + avatarRadius24dp, + animateAvatars + ) + } + + fun setMessage( + notificationViewData: NotificationViewData.Concrete, + listener: LinkListener, + animateEmojis: Boolean + ) { + val statusViewData = notificationViewData.statusViewData + val displayName = notificationViewData.account.name.unicodeWrap() + val type = notificationViewData.type + val context = binding.notificationTopText.context + val format: String + val icon: Drawable? + when (type) { + Notification.Type.FAVOURITE -> { + icon = getIconWithColor(context, R.drawable.ic_star_24dp, R.color.tusky_orange) + format = context.getString(R.string.notification_favourite_format) + } + Notification.Type.REBLOG -> { + icon = getIconWithColor(context, R.drawable.ic_repeat_24dp, R.color.tusky_blue) + format = context.getString(R.string.notification_reblog_format) + } + Notification.Type.STATUS -> { + icon = getIconWithColor(context, R.drawable.ic_home_24dp, R.color.tusky_blue) + format = context.getString(R.string.notification_subscription_format) + } + Notification.Type.UPDATE -> { + icon = getIconWithColor(context, R.drawable.ic_edit_24dp, R.color.tusky_blue) + format = context.getString(R.string.notification_update_format) + } + else -> { + icon = getIconWithColor(context, R.drawable.ic_star_24dp, R.color.tusky_orange) + format = context.getString(R.string.notification_favourite_format) + } + } + binding.notificationTopText.setCompoundDrawablesRelativeWithIntrinsicBounds( + icon, + null, + null, + null + ) + val wholeMessage = String.format(format, displayName).toSpannable() + val displayNameIndex = format.indexOf("%1\$s") + wholeMessage.setSpan( + StyleSpan(Typeface.BOLD), + displayNameIndex, + displayNameIndex + displayName.length, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + val emojifiedText = wholeMessage.emojify( + notificationViewData.account.emojis, + binding.notificationTopText, + animateEmojis + ) + binding.notificationTopText.text = emojifiedText + if (statusViewData != null) { + val hasSpoiler = statusViewData.status.spoilerText.isNotEmpty() + binding.notificationContentWarningDescription.visibility = + if (hasSpoiler) View.VISIBLE else View.GONE + binding.notificationContentWarningButton.visibility = + if (hasSpoiler) View.VISIBLE else View.GONE + if (statusViewData.isExpanded) { + binding.notificationContentWarningButton.setText( + R.string.post_content_warning_show_less + ) + } else { + binding.notificationContentWarningButton.setText( + R.string.post_content_warning_show_more + ) + } + binding.notificationContentWarningButton.setOnClickListener { + if (bindingAdapterPosition != RecyclerView.NO_POSITION) { + statusActionListener.onExpandedChange( + !statusViewData.isExpanded, + bindingAdapterPosition + ) + } + binding.notificationContent.visibility = + if (statusViewData.isExpanded) View.GONE else View.VISIBLE + } + setupContentAndSpoiler(listener, statusViewData, animateEmojis) + } + } + + private fun setupContentAndSpoiler( + listener: LinkListener, + statusViewData: StatusViewData.Concrete, + animateEmojis: Boolean + ) { + val shouldShowContentIfSpoiler = statusViewData.isExpanded + val hasSpoiler = statusViewData.status.spoilerText.isNotEmpty() + if (!shouldShowContentIfSpoiler && hasSpoiler) { + binding.notificationContent.visibility = View.GONE + } else { + binding.notificationContent.visibility = View.VISIBLE + } + val content = statusViewData.content + val emojis = statusViewData.actionable.emojis + if (statusViewData.isCollapsible && (statusViewData.isExpanded || !hasSpoiler)) { + binding.buttonToggleNotificationContent.setOnClickListener { + val position = bindingAdapterPosition + if (position != RecyclerView.NO_POSITION) { + statusActionListener.onContentCollapsedChange( + !statusViewData.isCollapsed, + position + ) + } + } + binding.buttonToggleNotificationContent.visibility = View.VISIBLE + if (statusViewData.isCollapsed) { + binding.buttonToggleNotificationContent.setText( + R.string.post_content_warning_show_more + ) + binding.notificationContent.filters = COLLAPSE_INPUT_FILTER + } else { + binding.buttonToggleNotificationContent.setText( + R.string.post_content_warning_show_less + ) + binding.notificationContent.filters = NO_INPUT_FILTER + } + } else { + binding.buttonToggleNotificationContent.visibility = View.GONE + binding.notificationContent.filters = NO_INPUT_FILTER + } + val emojifiedText = content.emojify( + emojis = emojis, + view = binding.notificationContent, + animate = animateEmojis + ) + setClickableText( + binding.notificationContent, + emojifiedText, + statusViewData.actionable.mentions, + statusViewData.actionable.tags, + listener + ) + val emojifiedContentWarning: CharSequence = statusViewData.status.spoilerText.emojify( + statusViewData.actionable.emojis, + binding.notificationContentWarningDescription, + animateEmojis + ) + binding.notificationContentWarningDescription.text = emojifiedContentWarning + } + + companion object { + private val COLLAPSE_INPUT_FILTER: Array<InputFilter> = arrayOf(SmartLengthInputFilter) + private val NO_INPUT_FILTER: Array<InputFilter> = arrayOf() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusViewHolder.kt new file mode 100644 index 0000000..fcc289e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/StatusViewHolder.kt @@ -0,0 +1,59 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.components.notifications + +import com.keylesspalace.tusky.adapter.StatusViewHolder +import com.keylesspalace.tusky.databinding.ItemStatusBinding +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.NotificationViewData + +internal class StatusViewHolder( + binding: ItemStatusBinding, + private val statusActionListener: StatusActionListener, + private val accountId: String +) : NotificationsViewHolder, StatusViewHolder(binding.root) { + + override fun bind( + viewData: NotificationViewData.Concrete, + payloads: List<*>, + statusDisplayOptions: StatusDisplayOptions + ) { + val statusViewData = viewData.statusViewData + if (statusViewData == null) { + /* in some very rare cases servers sends null status even though they should not */ + showStatusContent(false) + } else { + if (payloads.isEmpty()) { + showStatusContent(true) + } + setupWithStatus( + statusViewData, + statusActionListener, + statusDisplayOptions, + payloads.firstOrNull() + ) + } + if (viewData.type == Notification.Type.POLL) { + setPollInfo(accountId == viewData.account.id) + } else { + hideStatusInfo() + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/notifications/UnknownNotificationViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/notifications/UnknownNotificationViewHolder.kt new file mode 100644 index 0000000..3ccc2f2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/notifications/UnknownNotificationViewHolder.kt @@ -0,0 +1,36 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.components.notifications + +import com.keylesspalace.tusky.adapter.StatusViewHolder +import com.keylesspalace.tusky.databinding.ItemUnknownNotificationBinding +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.NotificationViewData + +internal class UnknownNotificationViewHolder( + binding: ItemUnknownNotificationBinding, +) : NotificationsViewHolder, StatusViewHolder(binding.root) { + + override fun bind( + viewData: NotificationViewData.Concrete, + payloads: List<*>, + statusDisplayOptions: StatusDisplayOptions + ) { + // nothing to do + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt new file mode 100644 index 0000000..c50d037 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/AccountPreferencesFragment.kt @@ -0,0 +1,378 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.preference + +import android.content.Intent +import android.graphics.Color +import android.os.Build +import android.os.Bundle +import android.util.Log +import androidx.annotation.DrawableRes +import androidx.lifecycle.lifecycleScope +import androidx.preference.PreferenceFragmentCompat +import at.connyduck.calladapter.networkresult.fold +import com.google.android.material.color.MaterialColors +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.TabPreferenceActivity +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.components.accountlist.AccountListActivity +import com.keylesspalace.tusky.components.domainblocks.DomainBlocksActivity +import com.keylesspalace.tusky.components.filters.FiltersActivity +import com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity +import com.keylesspalace.tusky.components.login.LoginActivity +import com.keylesspalace.tusky.components.systemnotifications.currentAccountNeedsMigration +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.settings.AccountPreferenceDataStore +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.settings.listPreference +import com.keylesspalace.tusky.settings.makePreferenceScreen +import com.keylesspalace.tusky.settings.preference +import com.keylesspalace.tusky.settings.preferenceCategory +import com.keylesspalace.tusky.settings.switchPreference +import com.keylesspalace.tusky.util.getInitialLanguages +import com.keylesspalace.tusky.util.getLocaleList +import com.keylesspalace.tusky.util.getTuskyDisplayName +import com.keylesspalace.tusky.util.makeIcon +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import com.keylesspalace.tusky.util.unsafeLazy +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeRes +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class AccountPreferencesFragment : PreferenceFragmentCompat() { + @Inject + lateinit var accountManager: AccountManager + + @Inject + lateinit var mastodonApi: MastodonApi + + @Inject + lateinit var eventHub: EventHub + + @Inject + lateinit var accountPreferenceDataStore: AccountPreferenceDataStore + + private val iconSize by unsafeLazy { + resources.getDimensionPixelSize( + R.dimen.preference_icon_size + ) + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + val context = requireContext() + makePreferenceScreen { + preference { + setTitle(R.string.pref_title_edit_notification_settings) + icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_notifications).apply { + sizeRes = R.dimen.preference_icon_size + colorInt = MaterialColors.getColor(context, R.attr.iconColor, Color.BLACK) + } + setOnPreferenceClickListener { + openNotificationSystemPrefs() + true + } + } + + preference { + setTitle(R.string.title_tab_preferences) + setIcon(R.drawable.ic_tabs) + setOnPreferenceClickListener { + val intent = Intent(context, TabPreferenceActivity::class.java) + activity?.startActivityWithSlideInAnimation(intent) + true + } + } + + preference { + setTitle(R.string.title_followed_hashtags) + setIcon(R.drawable.ic_hashtag) + setOnPreferenceClickListener { + val intent = Intent(context, FollowedTagsActivity::class.java) + activity?.startActivityWithSlideInAnimation(intent) + true + } + } + + preference { + setTitle(R.string.action_view_mutes) + setIcon(R.drawable.ic_mute_24dp) + setOnPreferenceClickListener { + val intent = Intent(context, AccountListActivity::class.java) + intent.putExtra("type", AccountListActivity.Type.MUTES) + activity?.startActivityWithSlideInAnimation(intent) + true + } + } + + preference { + setTitle(R.string.action_view_blocks) + icon = IconicsDrawable(context, GoogleMaterial.Icon.gmd_block).apply { + sizeRes = R.dimen.preference_icon_size + colorInt = MaterialColors.getColor(context, R.attr.iconColor, Color.BLACK) + } + setOnPreferenceClickListener { + val intent = Intent(context, AccountListActivity::class.java) + intent.putExtra("type", AccountListActivity.Type.BLOCKS) + activity?.startActivityWithSlideInAnimation(intent) + true + } + } + + preference { + setTitle(R.string.title_domain_mutes) + setIcon(R.drawable.ic_mute_24dp) + setOnPreferenceClickListener { + val intent = Intent(context, DomainBlocksActivity::class.java) + activity?.startActivityWithSlideInAnimation(intent) + true + } + } + + if (currentAccountNeedsMigration(accountManager)) { + preference { + setTitle(R.string.title_migration_relogin) + setIcon(R.drawable.ic_logout) + setOnPreferenceClickListener { + val intent = LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION) + activity?.startActivityWithSlideInAnimation(intent) + true + } + } + } + + preference { + setTitle(R.string.pref_title_timeline_filters) + setIcon(R.drawable.ic_filter_24dp) + setOnPreferenceClickListener { + launchFilterActivity() + true + } + } + + preferenceCategory(R.string.pref_publishing) { + listPreference { + setTitle(R.string.pref_default_post_privacy) + setEntries(R.array.post_privacy_names) + setEntryValues(R.array.post_privacy_values) + key = PrefKeys.DEFAULT_POST_PRIVACY + isSingleLineTitle = false + setSummaryProvider { entry } + val visibility = accountManager.activeAccount?.defaultPostPrivacy ?: Status.Visibility.PUBLIC + value = visibility.serverString + setIcon(getIconForVisibility(visibility)) + setOnPreferenceChangeListener { _, newValue -> + setIcon( + getIconForVisibility(Status.Visibility.byString(newValue as String)) + ) + syncWithServer(visibility = newValue) + true + } + } + + val activeAccount = accountManager.activeAccount + if (activeAccount != null) { + listPreference { + setTitle(R.string.pref_default_reply_privacy) + setEntries(R.array.post_privacy_names) + setEntryValues(R.array.post_privacy_values) + key = PrefKeys.DEFAULT_REPLY_PRIVACY + isSingleLineTitle = false + setSummaryProvider { entry } + val visibility = activeAccount.defaultReplyPrivacy + value = visibility.serverString + setIcon(getIconForVisibility(visibility)) + setOnPreferenceChangeListener { _, newValue -> + val newVisibility = Status.Visibility.byString(newValue as String) + setIcon(getIconForVisibility(newVisibility)) + activeAccount.defaultReplyPrivacy = newVisibility + accountManager.saveAccount(activeAccount) + viewLifecycleOwner.lifecycleScope.launch { + eventHub.dispatch(PreferenceChangedEvent(key)) + } + true + } + } + } + + listPreference { + val locales = + getLocaleList(getInitialLanguages(null, accountManager.activeAccount)) + setTitle(R.string.pref_default_post_language) + // Explicitly add "System default" to the start of the list + entries = ( + listOf(context.getString(R.string.system_default)) + locales.map { + it.getTuskyDisplayName(context) + } + ).toTypedArray() + entryValues = (listOf("") + locales.map { it.language }).toTypedArray() + key = PrefKeys.DEFAULT_POST_LANGUAGE + isSingleLineTitle = false + icon = makeIcon(requireContext(), GoogleMaterial.Icon.gmd_translate, iconSize) + value = accountManager.activeAccount?.defaultPostLanguage.orEmpty() + isPersistent = false // This will be entirely server-driven + setSummaryProvider { entry } + + setOnPreferenceChangeListener { _, newValue -> + syncWithServer(language = (newValue as String)) + true + } + } + + switchPreference { + setTitle(R.string.pref_default_media_sensitivity) + setIcon(R.drawable.ic_eye_24dp) + key = PrefKeys.DEFAULT_MEDIA_SENSITIVITY + isSingleLineTitle = false + val sensitivity = accountManager.activeAccount?.defaultMediaSensitivity ?: false + setDefaultValue(sensitivity) + setIcon(getIconForSensitivity(sensitivity)) + setOnPreferenceChangeListener { _, newValue -> + setIcon(getIconForSensitivity(newValue as Boolean)) + syncWithServer(sensitive = newValue) + true + } + } + } + + preferenceCategory(R.string.pref_title_timelines) { + // TODO having no activeAccount in this fragment does not really make sense, enforce it? + // All other locations here make it optional, however. + + switchPreference { + key = PrefKeys.MEDIA_PREVIEW_ENABLED + setTitle(R.string.pref_title_show_media_preview) + isSingleLineTitle = false + preferenceDataStore = accountPreferenceDataStore + } + + switchPreference { + key = PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA + setTitle(R.string.pref_title_alway_show_sensitive_media) + isSingleLineTitle = false + preferenceDataStore = accountPreferenceDataStore + } + + switchPreference { + key = PrefKeys.ALWAYS_OPEN_SPOILER + setTitle(R.string.pref_title_alway_open_spoiler) + isSingleLineTitle = false + preferenceDataStore = accountPreferenceDataStore + } + } + preferenceCategory(R.string.pref_title_per_timeline_preferences) { + preference { + setTitle(R.string.pref_title_post_tabs) + fragment = TabFilterPreferencesFragment::class.qualifiedName + } + } + } + } + + override fun onResume() { + super.onResume() + requireActivity().setTitle(R.string.action_view_account_preferences) + } + + private fun openNotificationSystemPrefs() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val intent = Intent() + intent.action = "android.settings.APP_NOTIFICATION_SETTINGS" + intent.putExtra("android.provider.extra.APP_PACKAGE", BuildConfig.APPLICATION_ID) + startActivity(intent) + } else { + activity?.let { + val intent = PreferencesActivity.newIntent( + it, + PreferencesActivity.NOTIFICATION_PREFERENCES + ) + it.startActivityWithSlideInAnimation(intent) + } + } + } + + private fun syncWithServer( + visibility: String? = null, + sensitive: Boolean? = null, + language: String? = null + ) { + // TODO these could also be "datastore backed" preferences (a ServerPreferenceDataStore); follow-up of issue #3204 + + viewLifecycleOwner.lifecycleScope.launch { + mastodonApi.accountUpdateSource(visibility, sensitive, language) + .fold({ account: Account -> + accountManager.activeAccount?.let { + it.defaultPostPrivacy = account.source?.privacy + ?: Status.Visibility.PUBLIC + it.defaultMediaSensitivity = account.source?.sensitive ?: false + it.defaultPostLanguage = language.orEmpty() + accountManager.saveAccount(it) + } + }, { t -> + Log.e("AccountPreferences", "failed updating settings on server", t) + showErrorSnackbar(visibility, sensitive) + }) + } + } + + private fun showErrorSnackbar(visibility: String?, sensitive: Boolean?) { + view?.let { view -> + Snackbar.make(view, R.string.pref_failed_to_sync, Snackbar.LENGTH_LONG) + .setAction(R.string.action_retry) { syncWithServer(visibility, sensitive) } + .show() + } + } + + @DrawableRes + private fun getIconForVisibility(visibility: Status.Visibility): Int { + return when (visibility) { + Status.Visibility.PRIVATE -> R.drawable.ic_lock_outline_24dp + + Status.Visibility.UNLISTED -> R.drawable.ic_lock_open_24dp + + else -> R.drawable.ic_public_24dp + } + } + + @DrawableRes + private fun getIconForSensitivity(sensitive: Boolean): Int { + return if (sensitive) { + R.drawable.ic_hide_media_24dp + } else { + R.drawable.ic_eye_24dp + } + } + + private fun launchFilterActivity() { + val intent = Intent(context, FiltersActivity::class.java) + (activity as? BaseActivity)?.startActivityWithSlideInAnimation(intent) + } + + companion object { + fun newInstance() = AccountPreferencesFragment() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt new file mode 100644 index 0000000..4b8eb03 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/NotificationPreferencesFragment.kt @@ -0,0 +1,218 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.preference + +import android.os.Bundle +import androidx.preference.PreferenceFragmentCompat +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.settings.makePreferenceScreen +import com.keylesspalace.tusky.settings.preferenceCategory +import com.keylesspalace.tusky.settings.switchPreference +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class NotificationPreferencesFragment : PreferenceFragmentCompat() { + + @Inject + lateinit var accountManager: AccountManager + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + val activeAccount = accountManager.activeAccount ?: return + val context = requireContext() + makePreferenceScreen { + switchPreference { + setTitle(R.string.pref_title_notifications_enabled) + key = PrefKeys.NOTIFICATIONS_ENABLED + isIconSpaceReserved = false + isChecked = activeAccount.notificationsEnabled + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsEnabled = newValue as Boolean } + if (NotificationHelper.areNotificationsEnabled(context, accountManager)) { + NotificationHelper.enablePullNotifications(context) + } else { + NotificationHelper.disablePullNotifications(context) + } + true + } + } + + preferenceCategory(R.string.pref_title_notification_filters) { category -> + category.dependency = PrefKeys.NOTIFICATIONS_ENABLED + category.isIconSpaceReserved = false + + switchPreference { + setTitle(R.string.pref_title_notification_filter_follows) + key = PrefKeys.NOTIFICATIONS_FILTER_FOLLOWS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsFollowed + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsFollowed = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_follow_requests) + key = PrefKeys.NOTIFICATION_FILTER_FOLLOW_REQUESTS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsFollowRequested + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsFollowRequested = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_reblogs) + key = PrefKeys.NOTIFICATION_FILTER_REBLOGS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsReblogged + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsReblogged = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_favourites) + key = PrefKeys.NOTIFICATION_FILTER_FAVS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsFavorited + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsFavorited = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_poll) + key = PrefKeys.NOTIFICATION_FILTER_POLLS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsPolls + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsPolls = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_subscriptions) + key = PrefKeys.NOTIFICATION_FILTER_SUBSCRIPTIONS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsSubscriptions + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsSubscriptions = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_sign_ups) + key = PrefKeys.NOTIFICATION_FILTER_SIGN_UPS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsSignUps + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsSignUps = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_updates) + key = PrefKeys.NOTIFICATION_FILTER_UPDATES + isIconSpaceReserved = false + isChecked = activeAccount.notificationsUpdates + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsUpdates = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_filter_reports) + key = PrefKeys.NOTIFICATION_FILTER_REPORTS + isIconSpaceReserved = false + isChecked = activeAccount.notificationsReports + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationsReports = newValue as Boolean } + true + } + } + } + + preferenceCategory(R.string.pref_title_notification_alerts) { category -> + category.dependency = PrefKeys.NOTIFICATIONS_ENABLED + category.isIconSpaceReserved = false + + switchPreference { + setTitle(R.string.pref_title_notification_alert_sound) + key = PrefKeys.NOTIFICATION_ALERT_SOUND + isIconSpaceReserved = false + isChecked = activeAccount.notificationSound + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationSound = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_alert_vibrate) + key = PrefKeys.NOTIFICATION_ALERT_VIBRATE + isIconSpaceReserved = false + isChecked = activeAccount.notificationVibration + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationVibration = newValue as Boolean } + true + } + } + + switchPreference { + setTitle(R.string.pref_title_notification_alert_light) + key = PrefKeys.NOTIFICATION_ALERT_LIGHT + isIconSpaceReserved = false + isChecked = activeAccount.notificationLight + setOnPreferenceChangeListener { _, newValue -> + updateAccount { it.notificationLight = newValue as Boolean } + true + } + } + } + } + } + + private inline fun updateAccount(changer: (AccountEntity) -> Unit) { + accountManager.activeAccount?.let { account -> + changer(account) + accountManager.saveAccount(account) + } + } + + override fun onResume() { + super.onResume() + requireActivity().setTitle(R.string.pref_title_edit_notification_settings) + } + + companion object { + fun newInstance(): NotificationPreferencesFragment { + return NotificationPreferencesFragment() + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt new file mode 100644 index 0000000..383ea2c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesActivity.kt @@ -0,0 +1,181 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.preference + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import androidx.activity.OnBackPressedCallback +import androidx.fragment.app.Fragment +import androidx.fragment.app.commit +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.MainActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.databinding.ActivityPreferencesBinding +import com.keylesspalace.tusky.settings.AppTheme +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.settings.PrefKeys.APP_THEME +import com.keylesspalace.tusky.util.getNonNullString +import com.keylesspalace.tusky.util.setAppNightMode +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class PreferencesActivity : + BaseActivity(), + SharedPreferences.OnSharedPreferenceChangeListener, + PreferenceFragmentCompat.OnPreferenceStartFragmentCallback { + + @Inject + lateinit var eventHub: EventHub + + private val restartActivitiesOnBackPressedCallback = object : OnBackPressedCallback(false) { + override fun handleOnBackPressed() { + /* Switching themes won't actually change the theme of activities on the back stack. + * Either the back stack activities need to all be recreated, or do the easier thing, which + * is hijack the back button press and use it to launch a new MainActivity and clear the + * back stack. */ + val intent = Intent(this@PreferencesActivity, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + startActivityWithSlideInAnimation(intent) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val binding = ActivityPreferencesBinding.inflate(layoutInflater) + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + val preferenceType = intent.getIntExtra(EXTRA_PREFERENCE_TYPE, 0) + + val fragmentTag = "preference_fragment_$preferenceType" + + val fragment: Fragment = supportFragmentManager.findFragmentByTag(fragmentTag) + ?: when (preferenceType) { + GENERAL_PREFERENCES -> PreferencesFragment.newInstance() + ACCOUNT_PREFERENCES -> AccountPreferencesFragment.newInstance() + NOTIFICATION_PREFERENCES -> NotificationPreferencesFragment.newInstance() + else -> throw IllegalArgumentException("preferenceType not known") + } + + supportFragmentManager.commit { + replace(R.id.fragment_container, fragment, fragmentTag) + } + + onBackPressedDispatcher.addCallback(this, restartActivitiesOnBackPressedCallback) + restartActivitiesOnBackPressedCallback.isEnabled = intent.extras?.getBoolean( + EXTRA_RESTART_ON_BACK + ) ?: savedInstanceState?.getBoolean(EXTRA_RESTART_ON_BACK, false) ?: false + } + + override fun onPreferenceStartFragment( + caller: PreferenceFragmentCompat, + pref: Preference + ): Boolean { + val args = pref.extras + val fragment = supportFragmentManager.fragmentFactory.instantiate( + classLoader, + pref.fragment!! + ) + fragment.arguments = args + supportFragmentManager.commit { + setCustomAnimations( + R.anim.activity_open_enter, + R.anim.activity_open_exit, + R.anim.activity_close_enter, + R.anim.activity_close_exit + ) + replace(R.id.fragment_container, fragment) + addToBackStack(null) + } + return true + } + + override fun onResume() { + super.onResume() + preferences.registerOnSharedPreferenceChangeListener(this) + } + + override fun onPause() { + super.onPause() + preferences.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onSaveInstanceState(outState: Bundle) { + outState.putBoolean(EXTRA_RESTART_ON_BACK, restartActivitiesOnBackPressedCallback.isEnabled) + super.onSaveInstanceState(outState) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + sharedPreferences ?: return + key ?: return + when (key) { + APP_THEME -> { + val theme = sharedPreferences.getNonNullString(APP_THEME, AppTheme.DEFAULT.value) + Log.d("activeTheme", theme) + setAppNightMode(theme) + + restartActivitiesOnBackPressedCallback.isEnabled = true + this.recreate() + } + PrefKeys.UI_TEXT_SCALE_RATIO -> { + restartActivitiesOnBackPressedCallback.isEnabled = true + this.recreate() + } + PrefKeys.STATUS_TEXT_SIZE, PrefKeys.ABSOLUTE_TIME_VIEW, PrefKeys.SHOW_BOT_OVERLAY, PrefKeys.ANIMATE_GIF_AVATARS, PrefKeys.USE_BLURHASH, + PrefKeys.SHOW_SELF_USERNAME, PrefKeys.SHOW_CARDS_IN_TIMELINES, PrefKeys.CONFIRM_REBLOGS, PrefKeys.CONFIRM_FAVOURITES, + PrefKeys.ENABLE_SWIPE_FOR_TABS, PrefKeys.MAIN_NAV_POSITION, PrefKeys.HIDE_TOP_TOOLBAR, PrefKeys.SHOW_STATS_INLINE -> { + restartActivitiesOnBackPressedCallback.isEnabled = true + } + } + lifecycleScope.launch { + eventHub.dispatch(PreferenceChangedEvent(key)) + } + } + + companion object { + @Suppress("unused") + private const val TAG = "PreferencesActivity" + const val GENERAL_PREFERENCES = 0 + const val ACCOUNT_PREFERENCES = 1 + const val NOTIFICATION_PREFERENCES = 2 + private const val EXTRA_PREFERENCE_TYPE = "EXTRA_PREFERENCE_TYPE" + private const val EXTRA_RESTART_ON_BACK = "restart" + + @JvmStatic + fun newIntent(context: Context, preferenceType: Int): Intent { + val intent = Intent(context, PreferencesActivity::class.java) + intent.putExtra(EXTRA_PREFERENCE_TYPE, preferenceType) + return intent + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt new file mode 100644 index 0000000..3ca6ca6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/PreferencesFragment.kt @@ -0,0 +1,332 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.preference + +import android.os.Bundle +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.settings.AppTheme +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.settings.emojiPreference +import com.keylesspalace.tusky.settings.listPreference +import com.keylesspalace.tusky.settings.makePreferenceScreen +import com.keylesspalace.tusky.settings.preference +import com.keylesspalace.tusky.settings.preferenceCategory +import com.keylesspalace.tusky.settings.sliderPreference +import com.keylesspalace.tusky.settings.switchPreference +import com.keylesspalace.tusky.util.LocaleManager +import com.keylesspalace.tusky.util.deserialize +import com.keylesspalace.tusky.util.makeIcon +import com.keylesspalace.tusky.util.serialize +import com.keylesspalace.tusky.util.unsafeLazy +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import dagger.hilt.android.AndroidEntryPoint +import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference +import javax.inject.Inject + +@AndroidEntryPoint +class PreferencesFragment : PreferenceFragmentCompat() { + + @Inject + lateinit var accountManager: AccountManager + + @Inject + lateinit var localeManager: LocaleManager + + private val iconSize by unsafeLazy { + resources.getDimensionPixelSize( + R.dimen.preference_icon_size + ) + } + + enum class ReadingOrder { + /** User scrolls up, reading statuses oldest to newest */ + OLDEST_FIRST, + + /** User scrolls down, reading statuses newest to oldest. Default behaviour. */ + NEWEST_FIRST; + + companion object { + fun from(s: String?): ReadingOrder { + s ?: return NEWEST_FIRST + + return try { + valueOf(s.uppercase()) + } catch (_: Throwable) { + NEWEST_FIRST + } + } + } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + makePreferenceScreen { + preferenceCategory(R.string.pref_title_appearance_settings) { + listPreference { + setDefaultValue(AppTheme.DEFAULT.value) + setEntries(R.array.app_theme_names) + entryValues = AppTheme.stringValues() + key = PrefKeys.APP_THEME + setSummaryProvider { entry } + setTitle(R.string.pref_title_app_theme) + icon = makeIcon(GoogleMaterial.Icon.gmd_palette) + } + + emojiPreference(requireActivity()) { + setTitle(R.string.emoji_style) + icon = makeIcon(GoogleMaterial.Icon.gmd_sentiment_satisfied) + } + + listPreference { + setDefaultValue("default") + setEntries(R.array.language_entries) + setEntryValues(R.array.language_values) + key = PrefKeys.LANGUAGE + "_" // deliberately not the actual key, the real handling happens in LocaleManager + setSummaryProvider { entry } + setTitle(R.string.pref_title_language) + icon = makeIcon(GoogleMaterial.Icon.gmd_translate) + preferenceDataStore = localeManager + } + + sliderPreference { + key = PrefKeys.UI_TEXT_SCALE_RATIO + setDefaultValue(100F) + valueTo = 150F + valueFrom = 50F + stepSize = 5F + setTitle(R.string.pref_ui_text_size) + format = "%.0f%%" + decrementIcon = makeIcon(GoogleMaterial.Icon.gmd_zoom_out) + incrementIcon = makeIcon(GoogleMaterial.Icon.gmd_zoom_in) + icon = makeIcon(GoogleMaterial.Icon.gmd_format_size) + } + + listPreference { + setDefaultValue("medium") + setEntries(R.array.post_text_size_names) + setEntryValues(R.array.post_text_size_values) + key = PrefKeys.STATUS_TEXT_SIZE + setSummaryProvider { entry } + setTitle(R.string.pref_post_text_size) + icon = makeIcon(GoogleMaterial.Icon.gmd_format_size) + } + + listPreference { + setDefaultValue(ReadingOrder.NEWEST_FIRST.name) + setEntries(R.array.reading_order_names) + setEntryValues(R.array.reading_order_values) + key = PrefKeys.READING_ORDER + setSummaryProvider { entry } + setTitle(R.string.pref_title_reading_order) + icon = makeIcon(GoogleMaterial.Icon.gmd_sort) + } + + listPreference { + setDefaultValue("top") + setEntries(R.array.pref_main_nav_position_options) + setEntryValues(R.array.pref_main_nav_position_values) + key = PrefKeys.MAIN_NAV_POSITION + setSummaryProvider { entry } + setTitle(R.string.pref_main_nav_position) + } + + listPreference { + setDefaultValue("disambiguate") + setEntries(R.array.pref_show_self_username_names) + setEntryValues(R.array.pref_show_self_username_values) + key = PrefKeys.SHOW_SELF_USERNAME + setSummaryProvider { entry } + setTitle(R.string.pref_title_show_self_username) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.HIDE_TOP_TOOLBAR + setTitle(R.string.pref_title_hide_top_toolbar) + } + + switchPreference { + setDefaultValue(true) + key = PrefKeys.SHOW_NOTIFICATIONS_FILTER + setTitle(R.string.pref_title_show_notifications_filter) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.ABSOLUTE_TIME_VIEW + setTitle(R.string.pref_title_absolute_time) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(true) + key = PrefKeys.SHOW_BOT_OVERLAY + setTitle(R.string.pref_title_bot_overlay) + isSingleLineTitle = false + setIcon(R.drawable.ic_bot_24dp) + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.ANIMATE_GIF_AVATARS + setTitle(R.string.pref_title_animate_gif_avatars) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.ANIMATE_CUSTOM_EMOJIS + setTitle(R.string.pref_title_animate_custom_emojis) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(true) + key = PrefKeys.USE_BLURHASH + setTitle(R.string.pref_title_gradient_for_media) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.SHOW_CARDS_IN_TIMELINES + setTitle(R.string.pref_title_show_cards_in_timelines) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(true) + key = PrefKeys.CONFIRM_REBLOGS + setTitle(R.string.pref_title_confirm_reblogs) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.CONFIRM_FAVOURITES + setTitle(R.string.pref_title_confirm_favourites) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.CONFIRM_FOLLOWS + setTitle(R.string.pref_title_confirm_follows) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(true) + key = PrefKeys.ENABLE_SWIPE_FOR_TABS + setTitle(R.string.pref_title_enable_swipe_for_tabs) + isSingleLineTitle = false + } + + switchPreference { + setDefaultValue(false) + key = PrefKeys.SHOW_STATS_INLINE + setTitle(R.string.pref_title_show_stat_inline) + isSingleLineTitle = false + } + } + + preferenceCategory(R.string.pref_title_browser_settings) { + switchPreference { + setDefaultValue(false) + key = PrefKeys.CUSTOM_TABS + setTitle(R.string.pref_title_custom_tabs) + isSingleLineTitle = false + } + } + + preferenceCategory(R.string.pref_title_wellbeing_mode) { + switchPreference { + title = getString(R.string.limit_notifications) + setDefaultValue(false) + key = PrefKeys.WELLBEING_LIMITED_NOTIFICATIONS + setOnPreferenceChangeListener { _, value -> + for (account in accountManager.accounts) { + val notificationFilter = deserialize( + account.notificationsFilter + ).toMutableSet() + + if (value == true) { + notificationFilter.add(Notification.Type.FAVOURITE) + notificationFilter.add(Notification.Type.FOLLOW) + notificationFilter.add(Notification.Type.REBLOG) + } else { + notificationFilter.remove(Notification.Type.FAVOURITE) + notificationFilter.remove(Notification.Type.FOLLOW) + notificationFilter.remove(Notification.Type.REBLOG) + } + + account.notificationsFilter = serialize(notificationFilter) + accountManager.saveAccount(account) + } + true + } + } + + switchPreference { + title = getString(R.string.wellbeing_hide_stats_posts) + setDefaultValue(false) + key = PrefKeys.WELLBEING_HIDE_STATS_POSTS + } + + switchPreference { + title = getString(R.string.wellbeing_hide_stats_profile) + setDefaultValue(false) + key = PrefKeys.WELLBEING_HIDE_STATS_PROFILE + } + } + + preferenceCategory(R.string.pref_title_proxy_settings) { + preference { + setTitle(R.string.pref_title_http_proxy_settings) + fragment = ProxyPreferencesFragment::class.qualifiedName + summaryProvider = ProxyPreferencesFragment.SummaryProvider + } + } + } + } + + private fun makeIcon(icon: GoogleMaterial.Icon): IconicsDrawable { + return makeIcon(requireContext(), icon, iconSize) + } + + override fun onResume() { + super.onResume() + requireActivity().setTitle(R.string.action_view_preferences) + } + + override fun onDisplayPreferenceDialog(preference: Preference) { + if (!EmojiPickerPreference.onDisplayPreferenceDialog(this, preference)) { + super.onDisplayPreferenceDialog(preference) + } + } + + companion object { + fun newInstance(): PreferencesFragment { + return PreferencesFragment() + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt new file mode 100644 index 0000000..84f1336 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/ProxyPreferencesFragment.kt @@ -0,0 +1,120 @@ +/* Copyright 2018 Conny Duck + + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.preference + +import android.os.Bundle +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.settings.ProxyConfiguration +import com.keylesspalace.tusky.settings.ProxyConfiguration.Companion.MAX_PROXY_PORT +import com.keylesspalace.tusky.settings.ProxyConfiguration.Companion.MIN_PROXY_PORT +import com.keylesspalace.tusky.settings.makePreferenceScreen +import com.keylesspalace.tusky.settings.preferenceCategory +import com.keylesspalace.tusky.settings.switchPreference +import com.keylesspalace.tusky.settings.validatedEditTextPreference +import com.keylesspalace.tusky.util.getNonNullString +import kotlin.system.exitProcess + +class ProxyPreferencesFragment : PreferenceFragmentCompat() { + private var pendingRestart = false + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + makePreferenceScreen { + switchPreference { + setTitle(R.string.pref_title_http_proxy_enable) + isIconSpaceReserved = false + key = PrefKeys.HTTP_PROXY_ENABLED + setDefaultValue(false) + } + + preferenceCategory { category -> + category.dependency = PrefKeys.HTTP_PROXY_ENABLED + category.isIconSpaceReserved = false + + validatedEditTextPreference(null, ProxyConfiguration::isValidHostname) { + setTitle(R.string.pref_title_http_proxy_server) + key = PrefKeys.HTTP_PROXY_SERVER + isIconSpaceReserved = false + setSummaryProvider { text } + } + + val portErrorMessage = getString( + R.string.pref_title_http_proxy_port_message, + MIN_PROXY_PORT, + MAX_PROXY_PORT + ) + + validatedEditTextPreference( + portErrorMessage, + ProxyConfiguration::isValidProxyPort + ) { + setTitle(R.string.pref_title_http_proxy_port) + key = PrefKeys.HTTP_PROXY_PORT + isIconSpaceReserved = false + setSummaryProvider { text } + } + } + } + } + + override fun onResume() { + super.onResume() + requireActivity().setTitle(R.string.pref_title_http_proxy_settings) + } + + override fun onPause() { + super.onPause() + if (pendingRestart) { + pendingRestart = false + exitProcess(0) + } + } + + object SummaryProvider : Preference.SummaryProvider<Preference> { + override fun provideSummary(preference: Preference): CharSequence { + val sharedPreferences = preference.sharedPreferences + sharedPreferences ?: return "" + + if (!sharedPreferences.getBoolean(PrefKeys.HTTP_PROXY_ENABLED, false)) { + return preference.context.getString(R.string.pref_summary_http_proxy_disabled) + } + + val missing = preference.context.getString(R.string.pref_summary_http_proxy_missing) + + val server = sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_SERVER, missing) + val port = try { + sharedPreferences.getNonNullString(PrefKeys.HTTP_PROXY_PORT, "-1").toInt() + } catch (e: NumberFormatException) { + -1 + } + + if (port < MIN_PROXY_PORT || port > MAX_PROXY_PORT) { + val invalid = preference.context.getString(R.string.pref_summary_http_proxy_invalid) + return "$server:$invalid" + } + + return "$server:$port" + } + } + + companion object { + fun newInstance(): ProxyPreferencesFragment { + return ProxyPreferencesFragment() + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt new file mode 100644 index 0000000..032b80c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/preference/TabFilterPreferencesFragment.kt @@ -0,0 +1,90 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.preference + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.preference.PreferenceFragmentCompat +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.settings.AccountPreferenceDataStore +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.settings.makePreferenceScreen +import com.keylesspalace.tusky.settings.preferenceCategory +import com.keylesspalace.tusky.settings.switchPreference +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint +class TabFilterPreferencesFragment : PreferenceFragmentCompat() { + + @Inject + lateinit var accountPreferenceDataStore: AccountPreferenceDataStore + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + // Give view a background color so transitions show up correctly + return super.onCreateView(inflater, container, savedInstanceState).also { view -> + view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.colorBackground)) + } + } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + makePreferenceScreen { + preferenceCategory(R.string.title_home) { category -> + category.isIconSpaceReserved = false + + switchPreference { + setTitle(R.string.pref_title_show_boosts) + key = PrefKeys.TAB_FILTER_HOME_BOOSTS + preferenceDataStore = accountPreferenceDataStore + isIconSpaceReserved = false + } + + switchPreference { + setTitle(R.string.pref_title_show_replies) + key = PrefKeys.TAB_FILTER_HOME_REPLIES + preferenceDataStore = accountPreferenceDataStore + isIconSpaceReserved = false + } + + switchPreference { + setTitle(R.string.pref_title_show_self_boosts) + setSummary(R.string.pref_title_show_self_boosts_description) + key = PrefKeys.TAB_SHOW_HOME_SELF_BOOSTS + preferenceDataStore = accountPreferenceDataStore + isIconSpaceReserved = false + }.apply { dependency = PrefKeys.TAB_FILTER_HOME_BOOSTS } + } + } + } + + override fun onResume() { + super.onResume() + requireActivity().setTitle(R.string.pref_title_post_tabs) + } + + companion object { + fun newInstance(): TabFilterPreferencesFragment { + return TabFilterPreferencesFragment() + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt new file mode 100644 index 0000000..7cbb977 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportActivity.kt @@ -0,0 +1,144 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.report + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.activity.viewModels +import androidx.lifecycle.lifecycleScope +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.report.adapter.ReportPagerAdapter +import com.keylesspalace.tusky.databinding.ActivityReportBinding +import com.keylesspalace.tusky.util.viewBinding +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class ReportActivity : BottomSheetActivity() { + + private val viewModel: ReportViewModel by viewModels() + + private val binding by viewBinding(ActivityReportBinding::inflate) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val accountId = intent?.getStringExtra(ACCOUNT_ID) + val accountUserName = intent?.getStringExtra(ACCOUNT_USERNAME) + if (accountId.isNullOrBlank() || accountUserName.isNullOrBlank()) { + throw IllegalStateException( + "accountId ($accountId) or accountUserName ($accountUserName) is null" + ) + } + + viewModel.init(accountId, accountUserName, intent?.getStringExtra(STATUS_ID)) + + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) + + supportActionBar?.apply { + title = getString(R.string.report_username_format, viewModel.accountUserName) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + setHomeAsUpIndicator(R.drawable.ic_close_24dp) + } + + initViewPager() + if (savedInstanceState == null) { + viewModel.navigateTo(Screen.Statuses) + } + subscribeObservables() + } + + private fun initViewPager() { + binding.wizard.isUserInputEnabled = false + + // Odd workaround for text field losing focus on first focus + // (unfixed old bug: https://github.com/material-components/material-components-android/issues/500) + binding.wizard.offscreenPageLimit = 1 + + binding.wizard.adapter = ReportPagerAdapter(this) + } + + private fun subscribeObservables() { + lifecycleScope.launch { + viewModel.navigation.collect { screen -> + if (screen == null) return@collect + viewModel.navigated() + when (screen) { + Screen.Statuses -> showStatusesPage() + Screen.Note -> showNotesPage() + Screen.Done -> showDonePage() + Screen.Back -> showPreviousScreen() + Screen.Finish -> closeScreen() + } + } + } + + lifecycleScope.launch { + viewModel.checkUrl.collect { + if (!it.isNullOrBlank()) { + viewModel.urlChecked() + viewUrl(it) + } + } + } + } + + private fun showPreviousScreen() { + when (binding.wizard.currentItem) { + 0 -> closeScreen() + 1 -> showStatusesPage() + } + } + + private fun showDonePage() { + binding.wizard.currentItem = 2 + } + + private fun showNotesPage() { + binding.wizard.currentItem = 1 + } + + private fun closeScreen() { + finish() + } + + private fun showStatusesPage() { + binding.wizard.currentItem = 0 + } + + companion object { + private const val ACCOUNT_ID = "account_id" + private const val ACCOUNT_USERNAME = "account_username" + private const val STATUS_ID = "status_id" + + @JvmStatic + fun getIntent( + context: Context, + accountId: String, + userName: String, + statusId: String? = null + ) = Intent(context, ReportActivity::class.java) + .apply { + putExtra(ACCOUNT_ID, accountId) + putExtra(ACCOUNT_USERNAME, userName) + putExtra(STATUS_ID, statusId) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt new file mode 100644 index 0000000..644ed12 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/ReportViewModel.kt @@ -0,0 +1,240 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.report + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import androidx.paging.map +import at.connyduck.calladapter.networkresult.fold +import com.keylesspalace.tusky.appstore.BlockEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.MuteEvent +import com.keylesspalace.tusky.components.report.adapter.StatusesPagingSource +import com.keylesspalace.tusky.components.report.model.StatusViewState +import com.keylesspalace.tusky.entity.Relationship +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.Error +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.Resource +import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.toViewData +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +@HiltViewModel +@OptIn(ExperimentalCoroutinesApi::class) +class ReportViewModel @Inject constructor( + private val mastodonApi: MastodonApi, + private val eventHub: EventHub +) : ViewModel() { + + private val navigationMutable = MutableStateFlow(null as Screen?) + val navigation: StateFlow<Screen?> = navigationMutable.asStateFlow() + + private val muteStateMutable = MutableStateFlow(null as Resource<Boolean>?) + val muteState: StateFlow<Resource<Boolean>?> = muteStateMutable.asStateFlow() + + private val blockStateMutable = MutableStateFlow(null as Resource<Boolean>?) + val blockState: StateFlow<Resource<Boolean>?> = blockStateMutable.asStateFlow() + + private val reportingStateMutable = MutableStateFlow(null as Resource<Boolean>?) + var reportingState: StateFlow<Resource<Boolean>?> = reportingStateMutable.asStateFlow() + + private val checkUrlMutable = MutableStateFlow(null as String?) + val checkUrl: StateFlow<String?> = checkUrlMutable.asStateFlow() + + private val accountIdFlow = MutableSharedFlow<String>( + replay = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + + val statusesFlow = accountIdFlow.flatMapLatest { accountId -> + Pager( + initialKey = statusId, + config = PagingConfig( + pageSize = 20, + initialLoadSize = 20 + ), + pagingSourceFactory = { StatusesPagingSource(accountId, mastodonApi) } + ).flow + } + .map { pagingData -> + /* TODO: refactor reports to use the isShowingContent / isExpanded / isCollapsed attributes from StatusViewData.Concrete + instead of StatusViewState */ + pagingData.map { status -> status.toViewData(false, false, false) } + } + .cachedIn(viewModelScope) + + private val selectedIds = HashSet<String>() + val statusViewState = StatusViewState() + + var reportNote: String = "" + var isRemoteNotify = false + + private var statusId: String? = null + lateinit var accountUserName: String + lateinit var accountId: String + var isRemoteAccount: Boolean = false + var remoteServer: String? = null + + fun init(accountId: String, userName: String, statusId: String?) { + this.accountId = accountId + this.accountUserName = userName + this.statusId = statusId + statusId?.let { + selectedIds.add(it) + } + + isRemoteAccount = userName.contains('@') + if (isRemoteAccount) { + remoteServer = userName.substring(userName.indexOf('@') + 1) + } + + obtainRelationship() + + viewModelScope.launch { + accountIdFlow.emit(accountId) + } + } + + fun navigateTo(screen: Screen) { + navigationMutable.value = screen + } + + fun navigated() { + navigationMutable.value = null + } + + private fun obtainRelationship() { + val ids = listOf(accountId) + muteStateMutable.value = Loading() + blockStateMutable.value = Loading() + viewModelScope.launch { + mastodonApi.relationships(ids).fold( + { data -> + updateRelationship(data.getOrNull(0)) + }, + { + updateRelationship(null) + } + ) + } + } + + private fun updateRelationship(relationship: Relationship?) { + if (relationship != null) { + muteStateMutable.value = Success(relationship.muting) + blockStateMutable.value = Success(relationship.blocking) + } else { + muteStateMutable.value = Error(false) + blockStateMutable.value = Error(false) + } + } + + fun toggleMute() { + val alreadyMuted = muteStateMutable.value?.data == true + viewModelScope.launch { + if (alreadyMuted) { + mastodonApi.unmuteAccount(accountId) + } else { + mastodonApi.muteAccount(accountId) + }.fold( + { relationship -> + val muting = relationship.muting + muteStateMutable.value = Success(muting) + if (muting) { + eventHub.dispatch(MuteEvent(accountId)) + } + }, + { t -> + muteStateMutable.value = Error(false, t.message) + } + ) + } + + muteStateMutable.value = Loading() + } + + fun toggleBlock() { + val alreadyBlocked = blockStateMutable.value?.data == true + viewModelScope.launch { + if (alreadyBlocked) { + mastodonApi.unblockAccount(accountId) + } else { + mastodonApi.blockAccount(accountId) + }.fold({ relationship -> + val blocking = relationship.blocking + blockStateMutable.value = Success(blocking) + if (blocking) { + eventHub.dispatch(BlockEvent(accountId)) + } + }, { t -> + blockStateMutable.value = Error(false, t.message) + }) + } + blockStateMutable.value = Loading() + } + + fun doReport() { + reportingStateMutable.value = Loading() + viewModelScope.launch { + mastodonApi.report( + accountId, + selectedIds.toList(), + reportNote, + if (isRemoteAccount) isRemoteNotify else null + ) + .fold({ + reportingStateMutable.value = Success(true) + }, { error -> + reportingStateMutable.value = Error(cause = error) + }) + } + } + + fun checkClickedUrl(url: String?) { + checkUrlMutable.value = url + } + + fun urlChecked() { + checkUrlMutable.value = null + } + + fun setStatusChecked(status: Status, checked: Boolean) { + if (checked) { + selectedIds.add(status.id) + } else { + selectedIds.remove(status.id) + } + } + + fun isStatusChecked(id: String): Boolean { + return selectedIds.contains(id) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt new file mode 100644 index 0000000..fb0b15c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/Screen.kt @@ -0,0 +1,24 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.report + +enum class Screen { + Statuses, + Note, + Done, + Back, + Finish +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt new file mode 100644 index 0000000..64e38db --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/AdapterHandler.kt @@ -0,0 +1,27 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.report.adapter + +import android.view.View +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.viewdata.StatusViewData + +interface AdapterHandler : LinkListener { + fun showMedia(v: View?, status: StatusViewData.Concrete, idx: Int) + fun setStatusChecked(status: Status, isChecked: Boolean) + fun isStatusChecked(id: String): Boolean +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/ReportPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/ReportPagerAdapter.kt new file mode 100644 index 0000000..fa5acc2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/ReportPagerAdapter.kt @@ -0,0 +1,36 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.report.adapter + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.keylesspalace.tusky.components.report.fragments.ReportDoneFragment +import com.keylesspalace.tusky.components.report.fragments.ReportNoteFragment +import com.keylesspalace.tusky.components.report.fragments.ReportStatusesFragment + +class ReportPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) { + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> ReportStatusesFragment.newInstance() + 1 -> ReportNoteFragment.newInstance() + 2 -> ReportDoneFragment.newInstance() + else -> throw IllegalArgumentException("Unknown page index: $position") + } + } + + override fun getItemCount() = 3 +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt new file mode 100644 index 0000000..2394075 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusViewHolder.kt @@ -0,0 +1,242 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.report.adapter + +import android.text.Spanned +import android.text.TextUtils +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.report.model.StatusViewState +import com.keylesspalace.tusky.databinding.ItemReportStatusBinding +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.HashTag +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.util.AbsoluteTimeFormatter +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.StatusViewHelper +import com.keylesspalace.tusky.util.StatusViewHelper.Companion.COLLAPSE_INPUT_FILTER +import com.keylesspalace.tusky.util.StatusViewHelper.Companion.NO_INPUT_FILTER +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.getRelativeTimeSpanString +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.setClickableMentions +import com.keylesspalace.tusky.util.setClickableText +import com.keylesspalace.tusky.util.shouldTrimStatus +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.keylesspalace.tusky.viewdata.toViewData +import java.util.Date + +class StatusViewHolder( + private val binding: ItemReportStatusBinding, + private val statusDisplayOptions: StatusDisplayOptions, + private val viewState: StatusViewState, + private val adapterHandler: AdapterHandler, + private val getStatusForPosition: (Int) -> StatusViewData.Concrete? +) : RecyclerView.ViewHolder(binding.root) { + + private val mediaViewHeight = itemView.context.resources.getDimensionPixelSize( + R.dimen.status_media_preview_height + ) + private val statusViewHelper = StatusViewHelper(itemView) + private val absoluteTimeFormatter = AbsoluteTimeFormatter() + + private val previewListener = object : StatusViewHelper.MediaPreviewListener { + override fun onViewMedia(v: View?, idx: Int) { + viewdata()?.let { viewdata -> + adapterHandler.showMedia(v, viewdata, idx) + } + } + + override fun onContentHiddenChange(isShowing: Boolean) { + viewdata()?.id?.let { id -> + viewState.setMediaShow(id, isShowing) + } + } + } + + init { + binding.statusSelection.setOnCheckedChangeListener { _, isChecked -> + viewdata()?.let { viewdata -> + adapterHandler.setStatusChecked(viewdata.status, isChecked) + } + } + binding.statusMediaPreviewContainer.clipToOutline = true + } + + fun bind(viewData: StatusViewData.Concrete) { + binding.statusSelection.isChecked = adapterHandler.isStatusChecked(viewData.id) + + updateTextView() + + val sensitive = viewData.status.sensitive + + statusViewHelper.setMediasPreview( + statusDisplayOptions, + viewData.status.attachments, + sensitive, + previewListener, + viewState.isMediaShow(viewData.id, viewData.status.sensitive), + mediaViewHeight + ) + + statusViewHelper.setupPollReadonly( + viewData.status.poll.toViewData(), + viewData.status.emojis, + statusDisplayOptions + ) + setCreatedAt(viewData.status.createdAt) + } + + private fun updateTextView() { + viewdata()?.let { viewdata -> + setupCollapsedState( + shouldTrimStatus(viewdata.content), + viewState.isCollapsed(viewdata.id, true), + viewState.isContentShow(viewdata.id, viewdata.status.sensitive), + viewdata.status.spoilerText + ) + + if (viewdata.status.spoilerText.isBlank()) { + setTextVisible( + true, + viewdata.content, + viewdata.status.mentions, + viewdata.status.tags, + viewdata.status.emojis, + adapterHandler + ) + binding.statusContentWarningButton.hide() + binding.statusContentWarningDescription.hide() + } else { + val emojiSpoiler = viewdata.status.spoilerText.emojify( + viewdata.status.emojis, + binding.statusContentWarningDescription, + statusDisplayOptions.animateEmojis + ) + binding.statusContentWarningDescription.text = emojiSpoiler + binding.statusContentWarningDescription.show() + binding.statusContentWarningButton.show() + setContentWarningButtonText(viewState.isContentShow(viewdata.id, true)) + binding.statusContentWarningButton.setOnClickListener { + viewdata()?.let { viewdata -> + val contentShown = viewState.isContentShow(viewdata.id, true) + binding.statusContentWarningDescription.invalidate() + viewState.setContentShow(viewdata.id, !contentShown) + setTextVisible( + !contentShown, + viewdata.content, + viewdata.status.mentions, + viewdata.status.tags, + viewdata.status.emojis, + adapterHandler + ) + setContentWarningButtonText(!contentShown) + } + } + setTextVisible( + viewState.isContentShow(viewdata.id, true), + viewdata.content, + viewdata.status.mentions, + viewdata.status.tags, + viewdata.status.emojis, + adapterHandler + ) + } + } + } + + private fun setContentWarningButtonText(contentShown: Boolean) { + if (contentShown) { + binding.statusContentWarningButton.setText(R.string.post_content_warning_show_less) + } else { + binding.statusContentWarningButton.setText(R.string.post_content_warning_show_more) + } + } + + private fun setTextVisible( + expanded: Boolean, + content: Spanned, + mentions: List<Status.Mention>, + tags: List<HashTag>?, + emojis: List<Emoji>, + listener: LinkListener + ) { + if (expanded) { + val emojifiedText = content.emojify( + emojis, + binding.statusContent, + statusDisplayOptions.animateEmojis + ) + setClickableText(binding.statusContent, emojifiedText, mentions, tags, listener) + } else { + setClickableMentions(binding.statusContent, mentions, listener) + } + if (binding.statusContent.text.isNullOrBlank()) { + binding.statusContent.hide() + } else { + binding.statusContent.show() + } + } + + private fun setCreatedAt(createdAt: Date?) { + if (statusDisplayOptions.useAbsoluteTime) { + binding.timestampInfo.text = absoluteTimeFormatter.format(createdAt) + } else { + binding.timestampInfo.text = if (createdAt != null) { + val then = createdAt.time + val now = System.currentTimeMillis() + getRelativeTimeSpanString(binding.timestampInfo.context, then, now) + } else { + // unknown minutes~ + "?m" + } + } + } + + private fun setupCollapsedState( + collapsible: Boolean, + collapsed: Boolean, + expanded: Boolean, + spoilerText: String + ) { + /* input filter for TextViews have to be set before text */ + if (collapsible && (expanded || TextUtils.isEmpty(spoilerText))) { + binding.buttonToggleContent.setOnClickListener { + viewdata()?.let { viewdata -> + viewState.setCollapsed(viewdata.id, !collapsed) + updateTextView() + } + } + + binding.buttonToggleContent.show() + if (collapsed) { + binding.buttonToggleContent.setText(R.string.post_content_show_more) + binding.statusContent.filters = COLLAPSE_INPUT_FILTER + } else { + binding.buttonToggleContent.setText(R.string.post_content_show_less) + binding.statusContent.filters = NO_INPUT_FILTER + } + } else { + binding.buttonToggleContent.hide() + binding.statusContent.filters = NO_INPUT_FILTER + } + } + + private fun viewdata() = getStatusForPosition(bindingAdapterPosition) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt new file mode 100644 index 0000000..db5c9bb --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesAdapter.kt @@ -0,0 +1,72 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.report.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.components.report.model.StatusViewState +import com.keylesspalace.tusky.databinding.ItemReportStatusBinding +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.StatusViewData + +class StatusesAdapter( + private val statusDisplayOptions: StatusDisplayOptions, + private val statusViewState: StatusViewState, + private val adapterHandler: AdapterHandler +) : PagingDataAdapter<StatusViewData.Concrete, StatusViewHolder>(STATUS_COMPARATOR) { + + private val statusForPosition: (Int) -> StatusViewData.Concrete? = { position: Int -> + if (position != RecyclerView.NO_POSITION) getItem(position) else null + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder { + val binding = ItemReportStatusBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return StatusViewHolder( + binding, + statusDisplayOptions, + statusViewState, + adapterHandler, + statusForPosition + ) + } + + override fun onBindViewHolder(holder: StatusViewHolder, position: Int) { + getItem(position)?.let { status -> + holder.bind(status) + } + } + + companion object { + val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<StatusViewData.Concrete>() { + override fun areContentsTheSame( + oldItem: StatusViewData.Concrete, + newItem: StatusViewData.Concrete + ): Boolean = oldItem == newItem + + override fun areItemsTheSame( + oldItem: StatusViewData.Concrete, + newItem: StatusViewData.Concrete + ): Boolean = oldItem.id == newItem.id + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesPagingSource.kt new file mode 100644 index 0000000..9b9238e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/adapter/StatusesPagingSource.kt @@ -0,0 +1,94 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.report.adapter + +import android.util.Log +import androidx.paging.PagingSource +import androidx.paging.PagingState +import at.connyduck.calladapter.networkresult.getOrThrow +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope + +class StatusesPagingSource( + private val accountId: String, + private val mastodonApi: MastodonApi +) : PagingSource<String, Status>() { + + override fun getRefreshKey(state: PagingState<String, Status>): String? { + return state.anchorPosition?.let { anchorPosition -> + state.closestItemToPosition(anchorPosition)?.id + } + } + + override suspend fun load(params: LoadParams<String>): LoadResult<String, Status> { + val key = params.key + try { + val result = if (params is LoadParams.Refresh && key != null) { + // Use coroutineScope to ensure that one failed call will cancel the other one + // and the source Exception will be propagated locally. + coroutineScope { + val initialStatus = async { getSingleStatus(key) } + val additionalStatuses = + async { getStatusList(maxId = key, limit = params.loadSize - 1) } + listOf(initialStatus.await()) + additionalStatuses.await() + } + } else { + val maxId = if (params is LoadParams.Refresh || params is LoadParams.Append) { + params.key + } else { + null + } + + val minId = if (params is LoadParams.Prepend) { + params.key + } else { + null + } + + getStatusList(minId = minId, maxId = maxId, limit = params.loadSize) + } + return LoadResult.Page( + data = result, + prevKey = result.firstOrNull()?.id, + nextKey = result.lastOrNull()?.id + ) + } catch (e: Exception) { + Log.w("StatusesPagingSource", "failed to load statuses", e) + return LoadResult.Error(e) + } + } + + private suspend fun getSingleStatus(statusId: String): Status { + return mastodonApi.status(statusId).getOrThrow() + } + + private suspend fun getStatusList( + minId: String? = null, + maxId: String? = null, + limit: Int + ): List<Status> { + return mastodonApi.accountStatuses( + accountId = accountId, + maxId = maxId, + sinceId = null, + minId = minId, + limit = limit, + excludeReblogs = true + ).getOrThrow() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt new file mode 100644 index 0000000..6961cae --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportDoneFragment.kt @@ -0,0 +1,103 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.report.fragments + +import android.os.Bundle +import android.view.View +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.report.ReportViewModel +import com.keylesspalace.tusky.components.report.Screen +import com.keylesspalace.tusky.databinding.FragmentReportDoneBinding +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class ReportDoneFragment : Fragment(R.layout.fragment_report_done) { + + private val viewModel: ReportViewModel by activityViewModels() + + private val binding by viewBinding(FragmentReportDoneBinding::bind) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + binding.textReported.text = getString(R.string.report_sent_success, viewModel.accountUserName) + handleClicks() + subscribeObservables() + } + + private fun subscribeObservables() { + viewLifecycleOwner.lifecycleScope.launch { + viewModel.muteState.collect { + if (it == null) return@collect + if (it !is Loading) { + binding.buttonMute.show() + binding.progressMute.show() + } else { + binding.buttonMute.hide() + binding.progressMute.hide() + } + + binding.buttonMute.setText( + when (it.data) { + true -> R.string.action_unmute + else -> R.string.action_mute + } + ) + } + } + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.blockState.collect { + if (it == null) return@collect + if (it !is Loading) { + binding.buttonBlock.show() + binding.progressBlock.show() + } else { + binding.buttonBlock.hide() + binding.progressBlock.hide() + } + binding.buttonBlock.setText( + when (it.data) { + true -> R.string.action_unblock + else -> R.string.action_block + } + ) + } + } + } + + private fun handleClicks() { + binding.buttonDone.setOnClickListener { + viewModel.navigateTo(Screen.Finish) + } + binding.buttonBlock.setOnClickListener { + viewModel.toggleBlock() + } + binding.buttonMute.setOnClickListener { + viewModel.toggleMute() + } + } + + companion object { + fun newInstance() = ReportDoneFragment() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt new file mode 100644 index 0000000..87f5c17 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportNoteFragment.kt @@ -0,0 +1,135 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.report.fragments + +import android.os.Bundle +import android.view.View +import androidx.core.widget.doAfterTextChanged +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.lifecycleScope +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.report.ReportViewModel +import com.keylesspalace.tusky.components.report.Screen +import com.keylesspalace.tusky.databinding.FragmentReportNoteBinding +import com.keylesspalace.tusky.util.Error +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import dagger.hilt.android.AndroidEntryPoint +import java.io.IOException +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class ReportNoteFragment : Fragment(R.layout.fragment_report_note) { + + private val viewModel: ReportViewModel by activityViewModels() + + private val binding by viewBinding(FragmentReportNoteBinding::bind) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + fillViews() + handleChanges() + handleClicks() + subscribeObservables() + } + + private fun handleChanges() { + binding.editNote.doAfterTextChanged { + viewModel.reportNote = it?.toString().orEmpty() + } + binding.checkIsNotifyRemote.setOnCheckedChangeListener { _, isChecked -> + viewModel.isRemoteNotify = isChecked + } + } + + private fun fillViews() { + binding.editNote.setText(viewModel.reportNote) + + if (viewModel.isRemoteAccount) { + binding.checkIsNotifyRemote.show() + binding.reportDescriptionRemoteInstance.show() + } else { + binding.checkIsNotifyRemote.hide() + binding.reportDescriptionRemoteInstance.hide() + } + + if (viewModel.isRemoteAccount) { + binding.checkIsNotifyRemote.text = getString(R.string.report_remote_instance, viewModel.remoteServer) + } + binding.checkIsNotifyRemote.isChecked = viewModel.isRemoteNotify + } + + private fun subscribeObservables() { + viewLifecycleOwner.lifecycleScope.launch { + viewModel.reportingState.collect { + if (it == null) return@collect + when (it) { + is Success -> viewModel.navigateTo(Screen.Done) + is Loading -> showLoading() + is Error -> showError(it.cause) + } + } + } + } + + private fun showError(error: Throwable?) { + binding.editNote.isEnabled = true + binding.checkIsNotifyRemote.isEnabled = true + binding.buttonReport.isEnabled = true + binding.buttonBack.isEnabled = true + binding.progressBar.hide() + + Snackbar.make( + binding.buttonBack, + if (error is IOException) R.string.error_network else R.string.error_generic, + Snackbar.LENGTH_LONG + ) + .setAction(R.string.action_retry) { + sendReport() + } + .show() + } + + private fun sendReport() { + viewModel.doReport() + } + + private fun showLoading() { + binding.buttonReport.isEnabled = false + binding.buttonBack.isEnabled = false + binding.editNote.isEnabled = false + binding.checkIsNotifyRemote.isEnabled = false + binding.progressBar.show() + } + + private fun handleClicks() { + binding.buttonBack.setOnClickListener { + viewModel.navigateTo(Screen.Back) + } + + binding.buttonReport.setOnClickListener { + sendReport() + } + } + + companion object { + fun newInstance() = ReportNoteFragment() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt new file mode 100644 index 0000000..66a1c29 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/fragments/ReportStatusesFragment.kt @@ -0,0 +1,249 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.report.fragments + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.core.app.ActivityOptionsCompat +import androidx.core.view.MenuProvider +import androidx.core.view.ViewCompat +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.SimpleItemAnimator +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import com.google.android.material.color.MaterialColors +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.StatusListActivity +import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.components.account.AccountActivity +import com.keylesspalace.tusky.components.report.ReportViewModel +import com.keylesspalace.tusky.components.report.Screen +import com.keylesspalace.tusky.components.report.adapter.AdapterHandler +import com.keylesspalace.tusky.components.report.adapter.StatusesAdapter +import com.keylesspalace.tusky.databinding.FragmentReportStatusesBinding +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class ReportStatusesFragment : + Fragment(R.layout.fragment_report_statuses), + OnRefreshListener, + MenuProvider, + AdapterHandler { + + @Inject + lateinit var accountManager: AccountManager + + @Inject + lateinit var preferences: SharedPreferences + + private val viewModel: ReportViewModel by activityViewModels() + + private val binding by viewBinding(FragmentReportStatusesBinding::bind) + + private var adapter: StatusesAdapter? = null + + private var snackbarErrorRetry: Snackbar? = null + + override fun showMedia(v: View?, status: StatusViewData.Concrete, idx: Int) { + when (status.attachments[idx].type) { + Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> { + val attachments = AttachmentViewData.list(status) + val intent = ViewMediaActivity.newIntent(context, attachments, idx) + if (v != null) { + val url = status.attachments[idx].url + ViewCompat.setTransitionName(v, url) + val options = ActivityOptionsCompat.makeSceneTransitionAnimation( + requireActivity(), + v, + url + ) + startActivity(intent, options.toBundle()) + } else { + startActivity(intent) + } + } + + Attachment.Type.UNKNOWN -> { + } + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) + handleClicks() + initStatusesView() + binding.swipeRefreshLayout.setOnRefreshListener(this) + } + + override fun onDestroyView() { + // Clear the adapter to prevent leaking the View + adapter = null + snackbarErrorRetry = null + super.onDestroyView() + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.fragment_report_statuses, menu) + menu.findItem(R.id.action_refresh)?.apply { + icon = IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_refresh).apply { + sizeDp = 20 + colorInt = MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary) + } + } + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_refresh -> { + binding.swipeRefreshLayout.isRefreshing = true + onRefresh() + true + } + + else -> false + } + } + + override fun onRefresh() { + snackbarErrorRetry?.dismiss() + snackbarErrorRetry = null + adapter?.refresh() + } + + private fun initStatusesView() { + val statusDisplayOptions = StatusDisplayOptions( + animateAvatars = false, + mediaPreviewEnabled = accountManager.activeAccount?.mediaPreviewEnabled ?: true, + useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false), + showBotOverlay = false, + useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true), + cardViewMode = CardViewMode.NONE, + confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true), + confirmFavourites = preferences.getBoolean(PrefKeys.CONFIRM_FAVOURITES, false), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), + showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false), + showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia, + openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler + ) + + val adapter = StatusesAdapter(statusDisplayOptions, viewModel.statusViewState, this) + this.adapter = adapter + + binding.recyclerView.addItemDecoration( + DividerItemDecoration(requireContext(), DividerItemDecoration.VERTICAL) + ) + binding.recyclerView.layoutManager = LinearLayoutManager(requireContext()) + binding.recyclerView.adapter = adapter + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.statusesFlow.collectLatest { pagingData -> + adapter.submitData(pagingData) + } + } + + adapter.addLoadStateListener { loadState -> + if (loadState.refresh is LoadState.Error || + loadState.append is LoadState.Error || + loadState.prepend is LoadState.Error + ) { + showError(adapter) + } + + binding.progressBarBottom.visible(loadState.append == LoadState.Loading) + binding.progressBarTop.visible(loadState.prepend == LoadState.Loading) + binding.progressBarLoading.visible( + loadState.refresh == LoadState.Loading && !binding.swipeRefreshLayout.isRefreshing + ) + + if (loadState.refresh != LoadState.Loading) { + binding.swipeRefreshLayout.isRefreshing = false + } + } + } + + private fun showError(adapter: StatusesAdapter) { + if (snackbarErrorRetry?.isShown != true) { + snackbarErrorRetry = + Snackbar.make(binding.swipeRefreshLayout, R.string.failed_fetch_posts, Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.action_retry) { + adapter.retry() + }.also { + it.show() + } + } + } + + private fun handleClicks() { + binding.buttonCancel.setOnClickListener { + viewModel.navigateTo(Screen.Back) + } + + binding.buttonContinue.setOnClickListener { + viewModel.navigateTo(Screen.Note) + } + } + + override fun setStatusChecked(status: Status, isChecked: Boolean) { + viewModel.setStatusChecked(status, isChecked) + } + + override fun isStatusChecked(id: String): Boolean { + return viewModel.isStatusChecked(id) + } + + override fun onViewAccount(id: String) = startActivity( + AccountActivity.getIntent(requireContext(), id) + ) + + override fun onViewTag(tag: String) = startActivity( + StatusListActivity.newHashtagIntent(requireContext(), tag) + ) + + override fun onViewUrl(url: String) = viewModel.checkClickedUrl(url) + + companion object { + fun newInstance() = ReportStatusesFragment() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt b/app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt new file mode 100644 index 0000000..893838c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/report/model/StatusViewState.kt @@ -0,0 +1,54 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.report.model + +class StatusViewState { + private val mediaShownState = HashMap<String, Boolean>() + private val contentShownState = HashMap<String, Boolean>() + private val longContentCollapsedState = HashMap<String, Boolean>() + + fun isMediaShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled( + mediaShownState, + id, + !isSensitive + ) + fun setMediaShow(id: String, isShow: Boolean) = setStateEnabled(mediaShownState, id, isShow) + + fun isContentShow(id: String, isSensitive: Boolean): Boolean = isStateEnabled( + contentShownState, + id, + !isSensitive + ) + fun setContentShow(id: String, isShow: Boolean) = setStateEnabled(contentShownState, id, isShow) + + fun isCollapsed(id: String, isCollapsed: Boolean): Boolean = isStateEnabled( + longContentCollapsedState, + id, + isCollapsed + ) + fun setCollapsed(id: String, isCollapsed: Boolean) = + setStateEnabled(longContentCollapsedState, id, isCollapsed) + + private fun isStateEnabled(map: Map<String, Boolean>, id: String, def: Boolean): Boolean = + map[id] + ?: def + + private fun setStateEnabled(map: MutableMap<String, Boolean>, id: String, state: Boolean) = + map.put( + id, + state + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt new file mode 100644 index 0000000..d2289d4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusActivity.kt @@ -0,0 +1,183 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.scheduled + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.activity.viewModels +import androidx.appcompat.app.AlertDialog +import androidx.core.view.MenuProvider +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.StatusScheduledEvent +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.databinding.ActivityScheduledStatusBinding +import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.viewBinding +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class ScheduledStatusActivity : + BaseActivity(), + ScheduledStatusActionListener, + MenuProvider { + + @Inject + lateinit var eventHub: EventHub + + private val viewModel: ScheduledStatusViewModel by viewModels() + + private val binding by viewBinding(ActivityScheduledStatusBinding::inflate) + + private val adapter = ScheduledStatusAdapter(this) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(binding.root) + addMenuProvider(this) + + setSupportActionBar(binding.includedToolbar.toolbar) + supportActionBar?.run { + title = getString(R.string.title_scheduled_posts) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + binding.swipeRefreshLayout.setOnRefreshListener(this::refreshStatuses) + + binding.scheduledTootList.setHasFixedSize(true) + binding.scheduledTootList.layoutManager = LinearLayoutManager(this) + val divider = DividerItemDecoration(this, DividerItemDecoration.VERTICAL) + binding.scheduledTootList.addItemDecoration(divider) + binding.scheduledTootList.adapter = adapter + + lifecycleScope.launch { + viewModel.data.collectLatest { pagingData -> + adapter.submitData(pagingData) + } + } + + adapter.addLoadStateListener { loadState -> + if (loadState.refresh is LoadState.Error) { + binding.progressBar.hide() + binding.errorMessageView.show() + + val errorState = loadState.refresh as LoadState.Error + binding.errorMessageView.setup(errorState.error) { refreshStatuses() } + } + if (loadState.refresh != LoadState.Loading) { + binding.swipeRefreshLayout.isRefreshing = false + } + if (loadState.refresh is LoadState.NotLoading) { + binding.progressBar.hide() + if (adapter.itemCount == 0) { + binding.errorMessageView.setup( + R.drawable.elephant_friend_empty, + R.string.no_scheduled_posts + ) + binding.errorMessageView.show() + } else { + binding.errorMessageView.hide() + } + } + } + + lifecycleScope.launch { + eventHub.events.collect { event -> + if (event is StatusScheduledEvent) { + adapter.refresh() + } + } + } + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.activity_scheduled_status, menu) + menu.findItem(R.id.action_search)?.apply { + icon = IconicsDrawable(this@ScheduledStatusActivity, GoogleMaterial.Icon.gmd_search).apply { + sizeDp = 20 + colorInt = MaterialColors.getColor(binding.includedToolbar.toolbar, android.R.attr.textColorPrimary) + } + } + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_refresh -> { + binding.swipeRefreshLayout.isRefreshing = true + refreshStatuses() + true + } + else -> false + } + } + + private fun refreshStatuses() { + adapter.refresh() + } + + override fun edit(item: ScheduledStatus) { + val intent = ComposeActivity.startIntent( + this, + ComposeActivity.ComposeOptions( + scheduledTootId = item.id, + content = item.params.text, + contentWarning = item.params.spoilerText, + mediaAttachments = item.mediaAttachments, + inReplyToId = item.params.inReplyToId, + visibility = item.params.visibility, + scheduledAt = item.scheduledAt, + sensitive = item.params.sensitive, + kind = ComposeActivity.ComposeKind.EDIT_SCHEDULED + ) + ) + startActivity(intent) + } + + override fun delete(item: ScheduledStatus) { + AlertDialog.Builder(this) + .setMessage(R.string.delete_scheduled_post_warning) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok) { _, _ -> + viewModel.deleteScheduledStatus(item) + } + .show() + } + + companion object { + fun newIntent(context: Context) = Intent(context, ScheduledStatusActivity::class.java) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusAdapter.kt new file mode 100644 index 0000000..a13a41e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusAdapter.kt @@ -0,0 +1,76 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.scheduled + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import com.keylesspalace.tusky.databinding.ItemScheduledStatusBinding +import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.util.BindingHolder + +interface ScheduledStatusActionListener { + fun edit(item: ScheduledStatus) + fun delete(item: ScheduledStatus) +} + +class ScheduledStatusAdapter( + val listener: ScheduledStatusActionListener +) : PagingDataAdapter<ScheduledStatus, BindingHolder<ItemScheduledStatusBinding>>( + object : DiffUtil.ItemCallback<ScheduledStatus>() { + override fun areItemsTheSame(oldItem: ScheduledStatus, newItem: ScheduledStatus): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: ScheduledStatus, + newItem: ScheduledStatus + ): Boolean { + return oldItem == newItem + } + } +) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemScheduledStatusBinding> { + val binding = ItemScheduledStatusBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return BindingHolder(binding) + } + + override fun onBindViewHolder( + holder: BindingHolder<ItemScheduledStatusBinding>, + position: Int + ) { + getItem(position)?.let { item -> + holder.binding.edit.isEnabled = true + holder.binding.delete.isEnabled = true + holder.binding.text.text = item.params.text + holder.binding.edit.setOnClickListener { + listener.edit(item) + } + holder.binding.delete.setOnClickListener { + listener.delete(item) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusPagingSource.kt new file mode 100644 index 0000000..57535b8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusPagingSource.kt @@ -0,0 +1,79 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.scheduled + +import android.util.Log +import androidx.paging.PagingSource +import androidx.paging.PagingState +import at.connyduck.calladapter.networkresult.getOrThrow +import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.network.MastodonApi + +class ScheduledStatusPagingSourceFactory( + private val mastodonApi: MastodonApi +) : () -> ScheduledStatusPagingSource { + + private val scheduledTootsCache = mutableListOf<ScheduledStatus>() + + private var pagingSource: ScheduledStatusPagingSource? = null + + override fun invoke(): ScheduledStatusPagingSource { + return ScheduledStatusPagingSource(mastodonApi, scheduledTootsCache).also { + pagingSource = it + } + } + + fun remove(status: ScheduledStatus) { + scheduledTootsCache.remove(status) + pagingSource?.invalidate() + } +} + +class ScheduledStatusPagingSource( + private val mastodonApi: MastodonApi, + private val scheduledStatusesCache: MutableList<ScheduledStatus> +) : PagingSource<String, ScheduledStatus>() { + + override fun getRefreshKey(state: PagingState<String, ScheduledStatus>): String? { + return null + } + + override suspend fun load(params: LoadParams<String>): LoadResult<String, ScheduledStatus> { + return if (params is LoadParams.Refresh && scheduledStatusesCache.isNotEmpty()) { + LoadResult.Page( + data = scheduledStatusesCache, + prevKey = null, + nextKey = scheduledStatusesCache.lastOrNull()?.id + ) + } else { + try { + val result = mastodonApi.scheduledStatuses( + maxId = params.key, + limit = params.loadSize + ).getOrThrow() + + LoadResult.Page( + data = result, + prevKey = null, + nextKey = result.lastOrNull()?.id + ) + } catch (e: Exception) { + Log.w("ScheduledStatuses", "Error loading scheduled statuses", e) + LoadResult.Error(e) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt new file mode 100644 index 0000000..2fa670f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/scheduled/ScheduledStatusViewModel.kt @@ -0,0 +1,59 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.scheduled + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import at.connyduck.calladapter.networkresult.fold +import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.network.MastodonApi +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.launch + +@HiltViewModel +class ScheduledStatusViewModel @Inject constructor( + val mastodonApi: MastodonApi +) : ViewModel() { + + private val pagingSourceFactory = ScheduledStatusPagingSourceFactory(mastodonApi) + + val data = Pager( + config = PagingConfig( + pageSize = 20, + initialLoadSize = 20 + ), + pagingSourceFactory = pagingSourceFactory + ).flow + .cachedIn(viewModelScope) + + fun deleteScheduledStatus(status: ScheduledStatus) { + viewModelScope.launch { + mastodonApi.deleteScheduledStatus(status.id).fold( + { + pagingSourceFactory.remove(status) + }, + { throwable -> + Log.w("ScheduledTootViewModel", "Error deleting scheduled status", throwable) + } + ) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt new file mode 100644 index 0000000..5ea7247 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchActivity.kt @@ -0,0 +1,163 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.search + +import android.app.SearchManager +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import androidx.activity.viewModels +import androidx.appcompat.widget.SearchView +import androidx.core.view.MenuProvider +import com.google.android.material.tabs.TabLayoutMediator +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.search.adapter.SearchPagerAdapter +import com.keylesspalace.tusky.databinding.ActivitySearchBinding +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.reduceSwipeSensitivity +import com.keylesspalace.tusky.util.viewBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class SearchActivity : BottomSheetActivity(), MenuProvider, SearchView.OnQueryTextListener { + + private val viewModel: SearchViewModel by viewModels() + + private val binding by viewBinding(ActivitySearchBinding::inflate) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + setSupportActionBar(binding.toolbar) + supportActionBar?.apply { + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + setDisplayShowTitleEnabled(false) + } + addMenuProvider(this) + setupPages() + handleIntent(intent) + } + + private fun setupPages() { + binding.pages.reduceSwipeSensitivity() + binding.pages.adapter = SearchPagerAdapter(this) + + val enableSwipeForTabs = preferences.getBoolean(PrefKeys.ENABLE_SWIPE_FOR_TABS, true) + binding.pages.isUserInputEnabled = enableSwipeForTabs + + TabLayoutMediator(binding.tabs, binding.pages) { + tab, position -> + tab.text = getPageTitle(position) + }.attach() + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + handleIntent(intent) + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.search_toolbar, menu) + val searchViewMenuItem = menu.findItem(R.id.action_search) + searchViewMenuItem.expandActionView() + val searchView = searchViewMenuItem.actionView as SearchView + setupSearchView(searchView) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return false + } + + private fun getPageTitle(position: Int): CharSequence { + return when (position) { + 0 -> getString(R.string.title_posts) + 1 -> getString(R.string.title_accounts) + 2 -> getString(R.string.title_hashtags_dialog) + else -> throw IllegalArgumentException("Unknown page index: $position") + } + } + + private fun handleIntent(intent: Intent) { + if (Intent.ACTION_SEARCH == intent.action) { + viewModel.currentQuery = intent.getStringExtra(SearchManager.QUERY).orEmpty() + viewModel.search(viewModel.currentQuery) + } + } + + private fun setupSearchView(searchView: SearchView) { + searchView.setIconifiedByDefault(false) + searchView.setSearchableInfo( + ( + getSystemService( + Context.SEARCH_SERVICE + ) as? SearchManager + )?.getSearchableInfo(componentName) + ) + + // SearchView has a bug. If it's displayed 'app:showAsAction="always"' it's too wide, + // pushing other icons (including the options menu '...' icon) off the edge of the + // screen. + // + // E.g., see: + // + // - https://stackoverflow.com/questions/41662373/android-toolbar-searchview-too-wide-to-move-other-items + // - https://stackoverflow.com/questions/51525088/how-to-control-size-of-a-searchview-in-toolbar + // - https://stackoverflow.com/questions/36976163/push-icons-away-when-expandig-searchview-in-android-toolbar + // - https://issuetracker.google.com/issues/36976484 + // + // The fix is to use 'app:showAsAction="ifRoom|collapseActionView"' and then immediately + // expand it after inflating. That sets the width correctly. + // + // But if you do that code in AppCompatDelegateImpl activates, and when the user presses + // the "Back" button the SearchView is first set to its collapsed state. The user has to + // press "Back" again to exit the activity. This is clearly unacceptable. + // + // It appears to be impossible to override this behaviour on API level < 33. + // + // SearchView does allow you to specify the maximum width. So take the screen width, + // subtract 48dp * 2 (for the menu icon and back icon on either side), convert to pixels, + // and use that. + val pxScreenWidth = resources.displayMetrics.widthPixels + val pxBuffer = ((48 * 2) * resources.displayMetrics.density).toInt() + searchView.maxWidth = pxScreenWidth - pxBuffer + + // Keep text that was entered also when switching to a different tab (before the search is executed) + searchView.setOnQueryTextListener(this) + searchView.setQuery(viewModel.currentSearchFieldContent ?: "", false) + + searchView.requestFocus() + } + + override fun onQueryTextSubmit(query: String?): Boolean { + return false + } + + override fun onQueryTextChange(newText: String?): Boolean { + viewModel.currentSearchFieldContent = newText + + return false + } + + companion object { + const val TAG = "SearchActivity" + fun getIntent(context: Context) = Intent(context, SearchActivity::class.java) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchType.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchType.kt new file mode 100644 index 0000000..235f8ce --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchType.kt @@ -0,0 +1,22 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.search + +enum class SearchType(val apiParameter: String) { + Status("statuses"), + Account("accounts"), + Hashtag("hashtags") +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt new file mode 100644 index 0000000..8c94d68 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/SearchViewModel.kt @@ -0,0 +1,258 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.search + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import at.connyduck.calladapter.networkresult.NetworkResult +import at.connyduck.calladapter.networkresult.fold +import at.connyduck.calladapter.networkresult.map +import at.connyduck.calladapter.networkresult.onFailure +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository +import com.keylesspalace.tusky.components.search.adapter.SearchPagingSourceFactory +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.entity.DeletedStatus +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.usecase.TimelineCases +import com.keylesspalace.tusky.util.toViewData +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.keylesspalace.tusky.viewdata.TranslationViewData +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.async +import kotlinx.coroutines.launch + +@HiltViewModel +class SearchViewModel @Inject constructor( + mastodonApi: MastodonApi, + private val timelineCases: TimelineCases, + private val accountManager: AccountManager, + private val instanceInfoRepository: InstanceInfoRepository, +) : ViewModel() { + + init { + instanceInfoRepository.precache() + } + + var currentQuery: String = "" + var currentSearchFieldContent: String? = null + + val activeAccount: AccountEntity? + get() = accountManager.activeAccount + + val mediaPreviewEnabled = activeAccount?.mediaPreviewEnabled ?: false + val alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false + val alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false + + private val loadedStatuses: MutableList<StatusViewData.Concrete> = mutableListOf() + + private val statusesPagingSourceFactory = + SearchPagingSourceFactory(mastodonApi, SearchType.Status, loadedStatuses) { + it.statuses.map { status -> + status.toViewData( + isShowingContent = alwaysShowSensitiveMedia || !status.actionableStatus.sensitive, + isExpanded = alwaysOpenSpoiler, + isCollapsed = true + ) + }.apply { + loadedStatuses.addAll(this) + } + } + private val accountsPagingSourceFactory = + SearchPagingSourceFactory(mastodonApi, SearchType.Account) { + it.accounts + } + private val hashtagsPagingSourceFactory = + SearchPagingSourceFactory(mastodonApi, SearchType.Hashtag) { + it.hashtags + } + + val statusesFlow = Pager( + config = PagingConfig( + pageSize = DEFAULT_LOAD_SIZE, + initialLoadSize = DEFAULT_LOAD_SIZE + ), + pagingSourceFactory = statusesPagingSourceFactory + ).flow + .cachedIn(viewModelScope) + + val accountsFlow = Pager( + config = PagingConfig( + pageSize = DEFAULT_LOAD_SIZE, + initialLoadSize = DEFAULT_LOAD_SIZE + ), + pagingSourceFactory = accountsPagingSourceFactory + ).flow + .cachedIn(viewModelScope) + + val hashtagsFlow = Pager( + config = PagingConfig( + pageSize = DEFAULT_LOAD_SIZE, + initialLoadSize = DEFAULT_LOAD_SIZE + ), + pagingSourceFactory = hashtagsPagingSourceFactory + ).flow + .cachedIn(viewModelScope) + + fun search(query: String) { + loadedStatuses.clear() + statusesPagingSourceFactory.newSearch(query) + accountsPagingSourceFactory.newSearch(query) + hashtagsPagingSourceFactory.newSearch(query) + } + + fun removeItem(statusViewData: StatusViewData.Concrete) { + viewModelScope.launch { + if (timelineCases.delete(statusViewData.id).isSuccess) { + if (loadedStatuses.remove(statusViewData)) { + statusesPagingSourceFactory.invalidate() + } + } + } + } + + fun expandedChange(statusViewData: StatusViewData.Concrete, expanded: Boolean) { + updateStatusViewData(statusViewData.copy(isExpanded = expanded)) + } + + fun reblog(statusViewData: StatusViewData.Concrete, reblog: Boolean) { + viewModelScope.launch { + timelineCases.reblog(statusViewData.id, reblog).fold({ + updateStatus( + statusViewData.status.copy( + reblogged = reblog, + reblog = statusViewData.status.reblog?.copy(reblogged = reblog) + ) + ) + }, { t -> + Log.d(TAG, "Failed to reblog status ${statusViewData.id}", t) + }) + } + } + + fun contentHiddenChange(statusViewData: StatusViewData.Concrete, isShowing: Boolean) { + updateStatusViewData(statusViewData.copy(isShowingContent = isShowing)) + } + + fun collapsedChange(statusViewData: StatusViewData.Concrete, collapsed: Boolean) { + updateStatusViewData(statusViewData.copy(isCollapsed = collapsed)) + } + + fun voteInPoll(statusViewData: StatusViewData.Concrete, choices: MutableList<Int>) { + val votedPoll = statusViewData.status.actionableStatus.poll!!.votedCopy(choices) + updateStatus(statusViewData.status.copy(poll = votedPoll)) + viewModelScope.launch { + timelineCases.voteInPoll(statusViewData.id, votedPoll.id, choices) + .onFailure { t -> Log.d(TAG, "Failed to vote in poll: ${statusViewData.id}", t) } + } + } + + fun favorite(statusViewData: StatusViewData.Concrete, isFavorited: Boolean) { + updateStatus(statusViewData.status.copy(favourited = isFavorited)) + viewModelScope.launch { + timelineCases.favourite(statusViewData.id, isFavorited) + } + } + + fun bookmark(statusViewData: StatusViewData.Concrete, isBookmarked: Boolean) { + updateStatus(statusViewData.status.copy(bookmarked = isBookmarked)) + viewModelScope.launch { + timelineCases.bookmark(statusViewData.id, isBookmarked) + } + } + + fun muteAccount(accountId: String, notifications: Boolean, duration: Int?) { + viewModelScope.launch { + timelineCases.mute(accountId, notifications, duration) + } + } + + fun pinAccount(status: Status, isPin: Boolean) { + viewModelScope.launch { + timelineCases.pin(status.id, isPin) + } + } + + fun blockAccount(accountId: String) { + viewModelScope.launch { + timelineCases.block(accountId) + } + } + + fun deleteStatusAsync(id: String): Deferred<NetworkResult<DeletedStatus>> { + return viewModelScope.async { + timelineCases.delete(id) + } + } + + fun muteConversation(statusViewData: StatusViewData.Concrete, mute: Boolean) { + updateStatus(statusViewData.status.copy(muted = mute)) + viewModelScope.launch { + timelineCases.muteConversation(statusViewData.id, mute) + } + } + + fun supportsTranslation(): Boolean = + instanceInfoRepository.cachedInstanceInfoOrFallback.translationEnabled == true + + suspend fun translate(statusViewData: StatusViewData.Concrete): NetworkResult<Unit> { + updateStatusViewData(statusViewData.copy(translation = TranslationViewData.Loading)) + return timelineCases.translate(statusViewData.actionableId) + .map { translation -> + updateStatusViewData( + statusViewData.copy( + translation = TranslationViewData.Loaded( + translation + ) + ) + ) + } + .onFailure { + updateStatusViewData(statusViewData.copy(translation = null)) + } + } + + fun untranslate(statusViewData: StatusViewData.Concrete) { + updateStatusViewData(statusViewData.copy(translation = null)) + } + + private fun updateStatusViewData(newStatusViewData: StatusViewData.Concrete) { + val idx = loadedStatuses.indexOfFirst { it.id == newStatusViewData.id } + if (idx >= 0) { + loadedStatuses[idx] = newStatusViewData + statusesPagingSourceFactory.invalidate() + } + } + + private fun updateStatus(newStatus: Status) { + val statusViewData = loadedStatuses.find { it.id == newStatus.id } + if (statusViewData != null) { + updateStatusViewData(statusViewData.copy(status = newStatus)) + } + } + + companion object { + private const val TAG = "SearchViewModel" + private const val DEFAULT_LOAD_SIZE = 20 + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt new file mode 100644 index 0000000..6efdcb3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchAccountsAdapter.kt @@ -0,0 +1,62 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.search.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import com.keylesspalace.tusky.adapter.AccountViewHolder +import com.keylesspalace.tusky.databinding.ItemAccountBinding +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.interfaces.LinkListener + +class SearchAccountsAdapter(private val linkListener: LinkListener, private val animateAvatars: Boolean, private val animateEmojis: Boolean, private val showBotOverlay: Boolean) : + PagingDataAdapter<TimelineAccount, AccountViewHolder>(ACCOUNT_COMPARATOR) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder { + val binding = ItemAccountBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + return AccountViewHolder(binding) + } + + override fun onBindViewHolder(holder: AccountViewHolder, position: Int) { + getItem(position)?.let { item -> + holder.apply { + setupWithAccount(item, animateAvatars, animateEmojis, showBotOverlay) + setupLinkListener(linkListener) + } + } + } + + companion object { + + val ACCOUNT_COMPARATOR = object : DiffUtil.ItemCallback<TimelineAccount>() { + override fun areContentsTheSame( + oldItem: TimelineAccount, + newItem: TimelineAccount + ): Boolean = oldItem == newItem + + override fun areItemsTheSame( + oldItem: TimelineAccount, + newItem: TimelineAccount + ): Boolean = oldItem.id == newItem.id + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt new file mode 100644 index 0000000..052ccc9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchHashtagsAdapter.kt @@ -0,0 +1,55 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.search.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import com.keylesspalace.tusky.databinding.ItemHashtagBinding +import com.keylesspalace.tusky.entity.HashTag +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.util.BindingHolder + +class SearchHashtagsAdapter(private val linkListener: LinkListener) : + PagingDataAdapter<HashTag, BindingHolder<ItemHashtagBinding>>(HASHTAG_COMPARATOR) { + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemHashtagBinding> { + val binding = ItemHashtagBinding.inflate(LayoutInflater.from(parent.context), parent, false) + return BindingHolder(binding) + } + + override fun onBindViewHolder(holder: BindingHolder<ItemHashtagBinding>, position: Int) { + getItem(position)?.let { (name) -> + holder.binding.root.text = String.format("#%s", name) + holder.binding.root.setOnClickListener { linkListener.onViewTag(name) } + } + } + + companion object { + + val HASHTAG_COMPARATOR = object : DiffUtil.ItemCallback<HashTag>() { + override fun areContentsTheSame(oldItem: HashTag, newItem: HashTag): Boolean = + oldItem.name == newItem.name + + override fun areItemsTheSame(oldItem: HashTag, newItem: HashTag): Boolean = + oldItem.name == newItem.name + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagerAdapter.kt new file mode 100644 index 0000000..9f30f9c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagerAdapter.kt @@ -0,0 +1,37 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.search.adapter + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.keylesspalace.tusky.components.search.fragments.SearchAccountsFragment +import com.keylesspalace.tusky.components.search.fragments.SearchHashtagsFragment +import com.keylesspalace.tusky.components.search.fragments.SearchStatusesFragment + +class SearchPagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) { + + override fun createFragment(position: Int): Fragment { + return when (position) { + 0 -> SearchStatusesFragment.newInstance() + 1 -> SearchAccountsFragment.newInstance() + 2 -> SearchHashtagsFragment.newInstance() + else -> throw IllegalArgumentException("Unknown page index: $position") + } + } + + override fun getItemCount() = 3 +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSource.kt new file mode 100644 index 0000000..d91f929 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSource.kt @@ -0,0 +1,83 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.search.adapter + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import at.connyduck.calladapter.networkresult.getOrThrow +import com.keylesspalace.tusky.components.search.SearchType +import com.keylesspalace.tusky.entity.SearchResult +import com.keylesspalace.tusky.network.MastodonApi + +class SearchPagingSource<T : Any>( + private val mastodonApi: MastodonApi, + private val searchType: SearchType, + private val searchRequest: String, + private val initialItems: List<T>?, + private val parser: (SearchResult) -> List<T> +) : PagingSource<Int, T>() { + + override fun getRefreshKey(state: PagingState<Int, T>): Int? { + return null + } + + override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> { + if (searchRequest.isEmpty()) { + return LoadResult.Page( + data = emptyList(), + prevKey = null, + nextKey = null + ) + } + + if (params.key == null && !initialItems.isNullOrEmpty()) { + return LoadResult.Page( + data = initialItems.toList(), + prevKey = null, + nextKey = initialItems.size + ) + } + + val currentKey = params.key ?: 0 + + try { + val data = mastodonApi.search( + query = searchRequest, + type = searchType.apiParameter, + resolve = true, + limit = params.loadSize, + offset = currentKey, + following = false + ).getOrThrow() + + val res = parser(data) + + val nextKey = if (res.isEmpty()) { + null + } else { + currentKey + res.size + } + + return LoadResult.Page( + data = res, + prevKey = null, + nextKey = nextKey + ) + } catch (e: Exception) { + return LoadResult.Error(e) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSourceFactory.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSourceFactory.kt new file mode 100644 index 0000000..f995d02 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchPagingSourceFactory.kt @@ -0,0 +1,53 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.search.adapter + +import com.keylesspalace.tusky.components.search.SearchType +import com.keylesspalace.tusky.entity.SearchResult +import com.keylesspalace.tusky.network.MastodonApi + +class SearchPagingSourceFactory<T : Any>( + private val mastodonApi: MastodonApi, + private val searchType: SearchType, + private val initialItems: List<T>? = null, + private val parser: (SearchResult) -> List<T> +) : () -> SearchPagingSource<T> { + + private var searchRequest: String = "" + + private var currentSource: SearchPagingSource<T>? = null + + override fun invoke(): SearchPagingSource<T> { + return SearchPagingSource( + mastodonApi = mastodonApi, + searchType = searchType, + searchRequest = searchRequest, + initialItems = initialItems, + parser = parser + ).also { source -> + currentSource = source + } + } + + fun newSearch(newSearchRequest: String) { + this.searchRequest = newSearchRequest + currentSource?.invalidate() + } + + fun invalidate() { + currentSource?.invalidate() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt new file mode 100644 index 0000000..1d3cabe --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/adapter/SearchStatusesAdapter.kt @@ -0,0 +1,59 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.search.adapter + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.StatusViewHolder +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.StatusViewData + +class SearchStatusesAdapter( + private val statusDisplayOptions: StatusDisplayOptions, + private val statusListener: StatusActionListener +) : PagingDataAdapter<StatusViewData.Concrete, StatusViewHolder>(STATUS_COMPARATOR) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_status, parent, false) + return StatusViewHolder(view) + } + + override fun onBindViewHolder(holder: StatusViewHolder, position: Int) { + getItem(position)?.let { item -> + holder.setupWithStatus(item, statusListener, statusDisplayOptions) + } + } + + companion object { + + val STATUS_COMPARATOR = object : DiffUtil.ItemCallback<StatusViewData.Concrete>() { + override fun areContentsTheSame( + oldItem: StatusViewData.Concrete, + newItem: StatusViewData.Concrete + ): Boolean = oldItem == newItem + + override fun areItemsTheSame( + oldItem: StatusViewData.Concrete, + newItem: StatusViewData.Concrete + ): Boolean = oldItem.id == newItem.id + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt new file mode 100644 index 0000000..a413fee --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchAccountsFragment.kt @@ -0,0 +1,62 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.search.fragments + +import android.content.SharedPreferences +import android.os.Bundle +import android.view.View +import androidx.paging.PagingData +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DividerItemDecoration +import com.keylesspalace.tusky.components.search.adapter.SearchAccountsAdapter +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.settings.PrefKeys +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow + +@AndroidEntryPoint +class SearchAccountsFragment : SearchFragment<TimelineAccount>() { + + @Inject + lateinit var preferences: SharedPreferences + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.searchRecyclerView.addItemDecoration( + DividerItemDecoration( + binding.searchRecyclerView.context, + DividerItemDecoration.VERTICAL + ) + ) + } + + override fun createAdapter(): PagingDataAdapter<TimelineAccount, *> { + return SearchAccountsAdapter( + this, + preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), + preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true) + ) + } + + override val data: Flow<PagingData<TimelineAccount>> + get() = viewModel.accountsFlow + + companion object { + fun newInstance() = SearchAccountsFragment() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt new file mode 100644 index 0000000..6dfe2c0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchFragment.kt @@ -0,0 +1,181 @@ +package com.keylesspalace.tusky.components.search.fragments + +import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import androidx.core.view.MenuProvider +import androidx.fragment.app.Fragment +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState +import androidx.paging.PagingData +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.SimpleItemAnimator +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.google.android.material.color.MaterialColors +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.StatusListActivity +import com.keylesspalace.tusky.components.account.AccountActivity +import com.keylesspalace.tusky.components.search.SearchViewModel +import com.keylesspalace.tusky.databinding.FragmentSearchBinding +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +abstract class SearchFragment<T : Any> : + Fragment(R.layout.fragment_search), + LinkListener, + SwipeRefreshLayout.OnRefreshListener, + MenuProvider { + + @Inject + lateinit var mastodonApi: MastodonApi + + protected val viewModel: SearchViewModel by activityViewModels() + + protected val binding by viewBinding(FragmentSearchBinding::bind) + + private var snackbarErrorRetry: Snackbar? = null + + abstract fun createAdapter(): PagingDataAdapter<T, *> + + abstract val data: Flow<PagingData<T>> + protected var adapter: PagingDataAdapter<T, *>? = null + + private var currentQuery: String = "" + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val adapter = initAdapter() + binding.swipeRefreshLayout.setOnRefreshListener(this) + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) + subscribeObservables(adapter) + } + + override fun onDestroyView() { + // Clear the adapter to prevent leaking the View + adapter = null + snackbarErrorRetry = null + super.onDestroyView() + } + + private fun subscribeObservables(adapter: PagingDataAdapter<T, *>) { + viewLifecycleOwner.lifecycleScope.launch { + data.collectLatest { pagingData -> + adapter.submitData(pagingData) + } + } + + adapter.addLoadStateListener { loadState -> + + if (loadState.refresh is LoadState.Error) { + showError(adapter) + } + + val isNewSearch = currentQuery != viewModel.currentQuery + + binding.searchProgressBar.visible( + loadState.refresh == LoadState.Loading && isNewSearch && !binding.swipeRefreshLayout.isRefreshing + ) + binding.searchRecyclerView.visible( + loadState.refresh is LoadState.NotLoading || !isNewSearch || binding.swipeRefreshLayout.isRefreshing + ) + + if (loadState.refresh != LoadState.Loading) { + binding.swipeRefreshLayout.isRefreshing = false + currentQuery = viewModel.currentQuery + } + + binding.progressBarBottom.visible(loadState.append == LoadState.Loading) + + binding.searchNoResultsText.visible( + loadState.refresh is LoadState.NotLoading && adapter.itemCount == 0 && viewModel.currentQuery.isNotEmpty() + ) + } + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.fragment_search, menu) + menu.findItem(R.id.action_refresh)?.apply { + icon = IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_refresh).apply { + sizeDp = 20 + colorInt = MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary) + } + } + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_refresh -> { + binding.swipeRefreshLayout.isRefreshing = true + onRefresh() + true + } + + else -> false + } + } + + private fun initAdapter(): PagingDataAdapter<T, *> { + binding.searchRecyclerView.layoutManager = LinearLayoutManager(binding.searchRecyclerView.context) + val adapter = createAdapter() + this.adapter = adapter + binding.searchRecyclerView.adapter = adapter + binding.searchRecyclerView.setHasFixedSize(true) + (binding.searchRecyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + return adapter + } + + private fun showError(adapter: PagingDataAdapter<T, *>) { + if (snackbarErrorRetry?.isShown != true) { + snackbarErrorRetry = + Snackbar.make(binding.root, R.string.failed_search, Snackbar.LENGTH_INDEFINITE) + .setAction(R.string.action_retry) { + snackbarErrorRetry = null + adapter.retry() + }.also { + it.show() + } + } + } + + override fun onViewAccount(id: String) { + bottomSheetActivity?.startActivityWithSlideInAnimation( + AccountActivity.getIntent(requireContext(), id) + ) + } + + override fun onViewTag(tag: String) { + bottomSheetActivity?.startActivityWithSlideInAnimation( + StatusListActivity.newHashtagIntent(requireContext(), tag) + ) + } + + override fun onViewUrl(url: String) { + bottomSheetActivity?.viewUrl(url) + } + + protected val bottomSheetActivity + get() = (activity as? BottomSheetActivity) + + override fun onRefresh() { + snackbarErrorRetry?.dismiss() + snackbarErrorRetry = null + adapter?.refresh() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchHashtagsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchHashtagsFragment.kt new file mode 100644 index 0000000..7bd3c9d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchHashtagsFragment.kt @@ -0,0 +1,49 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.search.fragments + +import android.os.Bundle +import android.view.View +import androidx.paging.PagingData +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DividerItemDecoration +import com.keylesspalace.tusky.components.search.adapter.SearchHashtagsAdapter +import com.keylesspalace.tusky.entity.HashTag +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.Flow + +@AndroidEntryPoint +class SearchHashtagsFragment : SearchFragment<HashTag>() { + + override val data: Flow<PagingData<HashTag>> + get() = viewModel.hashtagsFlow + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + binding.searchRecyclerView.addItemDecoration( + DividerItemDecoration( + binding.searchRecyclerView.context, + DividerItemDecoration.VERTICAL + ) + ) + } + + override fun createAdapter(): PagingDataAdapter<HashTag, *> = SearchHashtagsAdapter(this) + + companion object { + fun newInstance() = SearchHashtagsFragment() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt new file mode 100644 index 0000000..85d7c40 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/search/fragments/SearchStatusesFragment.kt @@ -0,0 +1,651 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.search.fragments + +import android.Manifest +import android.app.DownloadManager +import android.content.Intent +import android.content.SharedPreferences +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.util.Log +import android.view.View +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.PopupMenu +import androidx.core.app.ActivityOptionsCompat +import androidx.core.content.getSystemService +import androidx.core.view.ViewCompat +import androidx.lifecycle.lifecycleScope +import androidx.paging.PagingData +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import at.connyduck.calladapter.networkresult.fold +import at.connyduck.calladapter.networkresult.onFailure +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions +import com.keylesspalace.tusky.components.report.ReportActivity +import com.keylesspalace.tusky.components.search.adapter.SearchStatusesAdapter +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.Status.Mention +import com.keylesspalace.tusky.interfaces.AccountSelectionListener +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.copyToClipboard +import com.keylesspalace.tusky.util.openLink +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import com.keylesspalace.tusky.view.showMuteAccountDialog +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import com.keylesspalace.tusky.viewdata.StatusViewData +import dagger.hilt.android.AndroidEntryPoint +import java.util.Locale +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class SearchStatusesFragment : SearchFragment<StatusViewData.Concrete>(), StatusActionListener { + @Inject + lateinit var accountManager: AccountManager + + @Inject + lateinit var preferences: SharedPreferences + + override val data: Flow<PagingData<StatusViewData.Concrete>> + get() = viewModel.statusesFlow + + private var pendingMediaDownloads: List<String>? = null + + private val downloadAllMediaPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + pendingMediaDownloads?.let { downloadAllMedia(it) } + } else { + Toast.makeText( + context, + R.string.error_media_download_permission, + Toast.LENGTH_SHORT + ).show() + } + pendingMediaDownloads = null + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + pendingMediaDownloads = savedInstanceState?.getStringArrayList(PENDING_MEDIA_DOWNLOADS_STATE_KEY) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + pendingMediaDownloads?.let { + outState.putStringArrayList(PENDING_MEDIA_DOWNLOADS_STATE_KEY, ArrayList(it)) + } + } + + override fun createAdapter(): PagingDataAdapter<StatusViewData.Concrete, *> { + val statusDisplayOptions = StatusDisplayOptions( + animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + mediaPreviewEnabled = viewModel.mediaPreviewEnabled, + useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false), + showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true), + useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true), + cardViewMode = CardViewMode.NONE, + confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true), + confirmFavourites = preferences.getBoolean(PrefKeys.CONFIRM_FAVOURITES, false), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), + showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false), + showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia, + openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler + ) + val adapter = SearchStatusesAdapter(statusDisplayOptions, this) + + binding.searchRecyclerView.setAccessibilityDelegateCompat( + ListStatusAccessibilityDelegate(binding.searchRecyclerView, this) { pos -> + if (pos in 0 until adapter.itemCount) { + adapter.peek(pos) + } else { + null + } + } + ) + + binding.searchRecyclerView.addItemDecoration( + DividerItemDecoration( + binding.searchRecyclerView.context, + DividerItemDecoration.VERTICAL + ) + ) + binding.searchRecyclerView.layoutManager = + LinearLayoutManager(binding.searchRecyclerView.context) + return adapter + } + + override fun onContentHiddenChange(isShowing: Boolean, position: Int) { + adapter?.peek(position)?.let { + viewModel.contentHiddenChange(it, isShowing) + } + } + + override fun onReply(position: Int) { + adapter?.peek(position)?.let { status -> + reply(status) + } + } + + override fun onFavourite(favourite: Boolean, position: Int) { + adapter?.peek(position)?.let { status -> + viewModel.favorite(status, favourite) + } + } + + override fun onBookmark(bookmark: Boolean, position: Int) { + adapter?.peek(position)?.let { status -> + viewModel.bookmark(status, bookmark) + } + } + + override fun onMore(view: View, position: Int) { + adapter?.peek(position)?.let { + more(it, view, position) + } + } + + override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { + adapter?.peek(position)?.let { status -> + when (status.attachments[attachmentIndex].type) { + Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> { + val attachments = AttachmentViewData.list(status) + val intent = ViewMediaActivity.newIntent( + context, + attachments, + attachmentIndex + ) + if (view != null) { + val url = status.attachments[attachmentIndex].url + ViewCompat.setTransitionName(view, url) + val options = ActivityOptionsCompat.makeSceneTransitionAnimation( + requireActivity(), + view, + url + ) + startActivity(intent, options.toBundle()) + } else { + startActivity(intent) + } + } + + Attachment.Type.UNKNOWN -> { + context?.openLink(status.attachments[attachmentIndex].url) + } + } + } + } + + override fun onViewThread(position: Int) { + adapter?.peek(position)?.status?.let { status -> + val actionableStatus = status.actionableStatus + bottomSheetActivity?.viewThread(actionableStatus.id, actionableStatus.url) + } + } + + override fun onOpenReblog(position: Int) { + adapter?.peek(position)?.status?.let { status -> + bottomSheetActivity?.viewAccount(status.account.id) + } + } + + override fun onExpandedChange(expanded: Boolean, position: Int) { + adapter?.peek(position)?.let { + viewModel.expandedChange(it, expanded) + } + } + + override fun onLoadMore(position: Int) { + // Not possible here + } + + override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { + adapter?.peek(position)?.let { + viewModel.collapsedChange(it, isCollapsed) + } + } + + override fun onVoteInPoll(position: Int, choices: MutableList<Int>) { + adapter?.peek(position)?.let { + viewModel.voteInPoll(it, choices) + } + } + + override fun clearWarningAction(position: Int) {} + + private fun removeItem(position: Int) { + adapter?.peek(position)?.let { + viewModel.removeItem(it) + } + } + + override fun onReblog(reblog: Boolean, position: Int) { + adapter?.peek(position)?.let { status -> + viewModel.reblog(status, reblog) + } + } + + override fun onUntranslate(position: Int) { + adapter?.peek(position)?.let { + viewModel.untranslate(it) + } + } + + private fun reply(status: StatusViewData.Concrete) { + val actionableStatus = status.actionable + val mentionedUsernames = actionableStatus.mentions.map { it.username } + .toMutableSet() + .apply { + add(actionableStatus.account.username) + remove(viewModel.activeAccount?.username) + } + + val intent = ComposeActivity.startIntent( + requireContext(), + ComposeOptions( + inReplyToId = status.actionableId, + replyVisibility = actionableStatus.visibility, + contentWarning = actionableStatus.spoilerText, + mentionedUsernames = mentionedUsernames, + replyingStatusAuthor = actionableStatus.account.localUsername, + replyingStatusContent = status.content.toString(), + language = actionableStatus.language, + kind = ComposeActivity.ComposeKind.NEW + ) + ) + bottomSheetActivity?.startActivityWithSlideInAnimation(intent) + } + + private fun more(statusViewData: StatusViewData.Concrete, view: View, position: Int) { + val status = statusViewData.status + val id = status.actionableId + val accountId = status.actionableStatus.account.id + val accountUsername = status.actionableStatus.account.username + val statusUrl = status.actionableStatus.url + val loggedInAccountId = viewModel.activeAccount?.accountId + + val popup = PopupMenu(view.context, view) + val statusIsByCurrentUser = loggedInAccountId?.equals(accountId) == true + // Give a different menu depending on whether this is the user's own toot or not. + if (statusIsByCurrentUser) { + popup.inflate(R.menu.status_more_for_user) + val menu = popup.menu + menu.findItem(R.id.status_open_as).isVisible = !statusUrl.isNullOrBlank() + when (status.visibility) { + Status.Visibility.PUBLIC, Status.Visibility.UNLISTED -> { + val textId = + getString( + if (status.pinned) R.string.unpin_action else R.string.pin_action + ) + menu.add(0, R.id.pin, 1, textId) + } + + Status.Visibility.PRIVATE -> { + var reblogged = status.reblogged + if (status.reblog != null) reblogged = status.reblog.reblogged + menu.findItem(R.id.status_reblog_private).isVisible = !reblogged + menu.findItem(R.id.status_unreblog_private).isVisible = reblogged + } + + Status.Visibility.UNKNOWN, Status.Visibility.DIRECT -> { + } // Ignore + } + } else { + popup.inflate(R.menu.status_more) + val menu = popup.menu + menu.findItem(R.id.status_download_media).isVisible = status.attachments.isNotEmpty() + } + + val openAsItem = popup.menu.findItem(R.id.status_open_as) + val openAsText = bottomSheetActivity?.openAsText + if (openAsText == null) { + openAsItem.isVisible = false + } else { + openAsItem.title = openAsText + } + + val mutable = + statusIsByCurrentUser || accountIsInMentions(viewModel.activeAccount, status.mentions) + val muteConversationItem = popup.menu.findItem(R.id.status_mute_conversation).apply { + isVisible = mutable + } + if (mutable) { + muteConversationItem.setTitle( + if (status.muted) { + R.string.action_unmute_conversation + } else { + R.string.action_mute_conversation + } + ) + } + + // translation not there for your own posts + popup.menu.findItem(R.id.status_translate)?.let { translateItem -> + translateItem.isVisible = + !status.language.equals(Locale.getDefault().language, ignoreCase = true) && + viewModel.supportsTranslation() + translateItem.setTitle(if (statusViewData.translation != null) R.string.action_show_original else R.string.action_translate) + } + + popup.setOnMenuItemClickListener { item -> + when (item.itemId) { + R.id.post_share_content -> { + val statusToShare: Status = status.actionableStatus + + val sendIntent = Intent() + sendIntent.action = Intent.ACTION_SEND + + val stringToShare = statusToShare.account.username + + " - " + + statusToShare.content + sendIntent.putExtra(Intent.EXTRA_TEXT, stringToShare) + sendIntent.type = "text/plain" + startActivity( + Intent.createChooser( + sendIntent, + resources.getText(R.string.send_post_content_to) + ) + ) + return@setOnMenuItemClickListener true + } + + R.id.post_share_link -> { + val sendIntent = Intent() + sendIntent.action = Intent.ACTION_SEND + sendIntent.putExtra(Intent.EXTRA_TEXT, statusUrl) + sendIntent.type = "text/plain" + startActivity( + Intent.createChooser( + sendIntent, + resources.getText(R.string.send_post_link_to) + ) + ) + return@setOnMenuItemClickListener true + } + + R.id.status_copy_link -> { + statusUrl?.let { requireActivity().copyToClipboard(it, getString(R.string.url_copied)) } + return@setOnMenuItemClickListener true + } + + R.id.status_open_as -> { + showOpenAsDialog(statusUrl!!, item.title) + return@setOnMenuItemClickListener true + } + + R.id.status_download_media -> { + requestDownloadAllMedia(status) + return@setOnMenuItemClickListener true + } + + R.id.status_mute_conversation -> { + adapter?.peek(position)?.let { foundStatus -> + viewModel.muteConversation(foundStatus, !status.muted) + } + return@setOnMenuItemClickListener true + } + + R.id.status_mute -> { + onMute(accountId, accountUsername) + return@setOnMenuItemClickListener true + } + + R.id.status_block -> { + onBlock(accountId, accountUsername) + return@setOnMenuItemClickListener true + } + + R.id.status_report -> { + openReportPage(accountId, accountUsername, id) + return@setOnMenuItemClickListener true + } + + R.id.status_unreblog_private -> { + onReblog(false, position) + return@setOnMenuItemClickListener true + } + + R.id.status_reblog_private -> { + onReblog(true, position) + return@setOnMenuItemClickListener true + } + + R.id.status_delete -> { + showConfirmDeleteDialog(id, position) + return@setOnMenuItemClickListener true + } + + R.id.status_delete_and_redraft -> { + showConfirmEditDialog(id, position, status) + return@setOnMenuItemClickListener true + } + + R.id.status_edit -> { + editStatus(id, status) + return@setOnMenuItemClickListener true + } + + R.id.pin -> { + viewModel.pinAccount(status, !status.pinned) + return@setOnMenuItemClickListener true + } + + R.id.status_translate -> { + if (statusViewData.translation != null) { + viewModel.untranslate(statusViewData) + } else { + viewLifecycleOwner.lifecycleScope.launch { + viewModel.translate(statusViewData) + .onFailure { + Snackbar.make( + requireView(), + getString(R.string.ui_error_translate, it.message), + Snackbar.LENGTH_LONG + ).show() + } + } + } + } + } + false + } + popup.show() + } + + private fun onBlock(accountId: String, accountUsername: String) { + AlertDialog.Builder(requireContext()) + .setMessage(getString(R.string.dialog_block_warning, accountUsername)) + .setPositiveButton(android.R.string.ok) { _, _ -> viewModel.blockAccount(accountId) } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun onMute(accountId: String, accountUsername: String) { + showMuteAccountDialog( + this.requireActivity(), + accountUsername + ) { notifications, duration -> + viewModel.muteAccount(accountId, notifications, duration) + } + } + + private fun accountIsInMentions(account: AccountEntity?, mentions: List<Mention>): Boolean { + return mentions.firstOrNull { + account?.username == it.username && account.domain == Uri.parse(it.url)?.host + } != null + } + + private fun showOpenAsDialog(statusUrl: String, dialogTitle: CharSequence?) { + bottomSheetActivity?.showAccountChooserDialog( + dialogTitle, + false, + object : AccountSelectionListener { + override fun onAccountSelected(account: AccountEntity) { + bottomSheetActivity?.openAsAccount(statusUrl, account) + } + } + ) + } + + private fun downloadAllMedia(mediaUrls: List<String>) { + Toast.makeText(context, R.string.downloading_media, Toast.LENGTH_SHORT).show() + val downloadManager: DownloadManager = requireContext().getSystemService()!! + + for (url in mediaUrls) { + val uri = Uri.parse(url) + val request = DownloadManager.Request(uri) + request.setDestinationInExternalPublicDir( + Environment.DIRECTORY_DOWNLOADS, + uri.lastPathSegment + ) + downloadManager.enqueue(request) + } + } + + private fun requestDownloadAllMedia(status: Status) { + if (status.attachments.isEmpty()) { + return + } + val mediaUrls = status.attachments.map { it.url } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + pendingMediaDownloads = mediaUrls + downloadAllMediaPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } else { + downloadAllMedia(mediaUrls) + } + } + + private fun openReportPage(accountId: String, accountUsername: String, statusId: String) { + startActivity( + ReportActivity.getIntent(requireContext(), accountId, accountUsername, statusId) + ) + } + + private fun showConfirmDeleteDialog(id: String, position: Int) { + context?.let { + AlertDialog.Builder(it) + .setMessage(R.string.dialog_delete_post_warning) + .setPositiveButton(android.R.string.ok) { _, _ -> + viewModel.deleteStatusAsync(id) + removeItem(position) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + } + + private fun showConfirmEditDialog(id: String, position: Int, status: Status) { + context?.let { context -> + AlertDialog.Builder(context) + .setMessage(R.string.dialog_redraft_post_warning) + .setPositiveButton(android.R.string.ok) { _, _ -> + viewLifecycleOwner.lifecycleScope.launch { + viewModel.deleteStatusAsync(id).await().fold( + { deletedStatus -> + removeItem(position) + + val redraftStatus = if (deletedStatus.isEmpty) { + status.toDeletedStatus() + } else { + deletedStatus + } + + val intent = ComposeActivity.startIntent( + context, + ComposeOptions( + content = redraftStatus.text.orEmpty(), + inReplyToId = redraftStatus.inReplyToId, + visibility = redraftStatus.visibility, + contentWarning = redraftStatus.spoilerText, + mediaAttachments = redraftStatus.attachments, + sensitive = redraftStatus.sensitive, + poll = redraftStatus.poll?.toNewPoll(status.createdAt), + language = redraftStatus.language, + kind = ComposeActivity.ComposeKind.NEW + ) + ) + startActivity(intent) + }, + { error -> + Log.w("SearchStatusesFragment", "error deleting status", error) + Toast.makeText( + context, + R.string.error_generic, + Toast.LENGTH_SHORT + ).show() + } + ) + } + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + } + + private fun editStatus(id: String, status: Status) { + viewLifecycleOwner.lifecycleScope.launch { + mastodonApi.statusSource(id).fold( + { source -> + val composeOptions = ComposeOptions( + content = source.text, + inReplyToId = status.inReplyToId, + visibility = status.visibility, + contentWarning = source.spoilerText, + mediaAttachments = status.attachments, + sensitive = status.sensitive, + language = status.language, + statusId = source.id, + poll = status.poll?.toNewPoll(status.createdAt), + kind = ComposeActivity.ComposeKind.EDIT_POSTED + ) + startActivity(ComposeActivity.startIntent(requireContext(), composeOptions)) + }, + { + Snackbar.make( + requireView(), + getString(R.string.error_status_source_load), + Snackbar.LENGTH_SHORT + ).show() + } + ) + } + } + + companion object { + private const val PENDING_MEDIA_DOWNLOADS_STATE_KEY = "pending_media_downloads" + + fun newInstance() = SearchStatusesFragment() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationFetcher.kt b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationFetcher.kt new file mode 100644 index 0000000..20b5292 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationFetcher.kt @@ -0,0 +1,257 @@ +package com.keylesspalace.tusky.components.systemnotifications + +import android.app.NotificationManager +import android.content.Context +import android.util.Log +import androidx.annotation.WorkerThread +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.NewNotificationsEvent +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper.filterNotification +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.entity.Marker +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.HttpHeaderLink +import com.keylesspalace.tusky.util.isLessThan +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlin.math.min +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.delay + +/** Models next/prev links from the "Links" header in an API response */ +data class Links(val next: String?, val prev: String?) { + companion object { + fun from(linkHeader: String?): Links { + val links = HttpHeaderLink.parse(linkHeader) + return Links( + next = HttpHeaderLink.findByRelationType(links, "next")?.uri?.getQueryParameter( + "max_id" + ), + prev = HttpHeaderLink.findByRelationType(links, "prev")?.uri?.getQueryParameter( + "min_id" + ) + ) + } + } +} + +/** + * Fetch Mastodon notifications and show Android notifications, with summaries, for them. + * + * Should only be called by a worker thread. + * + * @see com.keylesspalace.tusky.worker.NotificationWorker + * @see <a href="https://developer.android.com/guide/background/persistent/threading/worker">Background worker</a> + */ +@WorkerThread +class NotificationFetcher @Inject constructor( + private val mastodonApi: MastodonApi, + private val accountManager: AccountManager, + @ApplicationContext private val context: Context, + private val eventHub: EventHub +) { + suspend fun fetchAndShow() { + for (account in accountManager.getAllAccountsOrderedByActive()) { + if (account.notificationsEnabled) { + try { + val notificationManager = context.getSystemService( + Context.NOTIFICATION_SERVICE + ) as NotificationManager + + // Create sorted list of new notifications + val notifications = fetchNewNotifications(account) + .filter { filterNotification(notificationManager, account, it) } + .sortedWith( + compareBy({ it.id.length }, { it.id }) + ) // oldest notifications first + .toMutableList() + + // TODO do this before filter above? But one could argue that (for example) a tab badge is also a notification + // (and should therefore adhere to the notification config). + eventHub.dispatch(NewNotificationsEvent(account.accountId, notifications)) + + // There's a maximum limit on the number of notifications an Android app + // can display. If the total number of notifications (current notifications, + // plus new ones) exceeds this then some newer notifications will be dropped. + // + // Err on the side of removing *older* notifications to make room for newer + // notifications. + val currentAndroidNotifications = notificationManager.activeNotifications + .sortedWith( + compareBy({ it.tag.length }, { it.tag }) + ) // oldest notifications first + + // Check to see if any notifications need to be removed + val toRemove = currentAndroidNotifications.size + notifications.size - MAX_NOTIFICATIONS + if (toRemove > 0) { + // Prefer to cancel old notifications first + currentAndroidNotifications.subList( + 0, + min(toRemove, currentAndroidNotifications.size) + ) + .forEach { notificationManager.cancel(it.tag, it.id) } + + // Still got notifications to remove? Trim the list of new notifications, + // starting with the oldest. + while (notifications.size > MAX_NOTIFICATIONS) { + notifications.removeAt(0) + } + } + + val notificationsByType = notifications.groupBy { it.type } + + // Make and send the new notifications + // TODO: Use the batch notification API available in NotificationManagerCompat + // 1.11 and up (https://developer.android.com/jetpack/androidx/releases/core#1.11.0-alpha01) + // when it is released. + + notificationsByType.forEach { notificationsGroup -> + notificationsGroup.value.forEach { notification -> + val androidNotification = NotificationHelper.make( + context, + notificationManager, + notification, + account, + notificationsGroup.value.size == 1 + ) + notificationManager.notify( + notification.id, + account.id.toInt(), + androidNotification + ) + + // Android will rate limit / drop notifications if they're posted too + // quickly. There is no indication to the user that this happened. + // See https://github.com/tuskyapp/Tusky/pull/3626#discussion_r1192963664 + delay(1000.milliseconds) + } + } + + NotificationHelper.updateSummaryNotifications( + context, + notificationManager, + account + ) + + accountManager.saveAccount(account) + } catch (e: Exception) { + Log.e(TAG, "Error while fetching notifications", e) + } + } + } + } + + /** + * Fetch new Mastodon Notifications and update the marker position. + * + * Here, "new" means "notifications with IDs newer than notifications the user has already + * seen." + * + * The "water mark" for Mastodon Notification IDs are stored in three places. + * + * - acccount.lastNotificationId -- the ID of the top-most notification when the user last + * left the Notifications tab. + * - The Mastodon "marker" API -- the ID of the most recent notification fetched here. + * - account.notificationMarkerId -- local version of the value from the Mastodon marker + * API, in case the Mastodon server does not implement that API. + * + * The user may have refreshed the "Notifications" tab and seen notifications newer than the + * ones that were last fetched here. So `lastNotificationId` takes precedence if it is greater + * than the marker. + */ + private suspend fun fetchNewNotifications(account: AccountEntity): List<Notification> { + val authHeader = String.format("Bearer %s", account.accessToken) + + // Figure out where to read from. Choose the most recent notification ID from: + // + // - The Mastodon marker API (if the server supports it) + // - account.notificationMarkerId + // - account.lastNotificationId + Log.d(TAG, "getting notification marker for ${account.fullName}") + val remoteMarkerId = fetchMarker(authHeader, account)?.lastReadId ?: "0" + val localMarkerId = account.notificationMarkerId + val markerId = if (remoteMarkerId.isLessThan( + localMarkerId + ) + ) { + localMarkerId + } else { + remoteMarkerId + } + val readingPosition = account.lastNotificationId + + var minId: String? = if (readingPosition.isLessThan(markerId)) markerId else readingPosition + Log.d(TAG, " remoteMarkerId: $remoteMarkerId") + Log.d(TAG, " localMarkerId: $localMarkerId") + Log.d(TAG, " readingPosition: $readingPosition") + + Log.d(TAG, "getting Notifications for ${account.fullName}, min_id: $minId") + + // Fetch all outstanding notifications + val notifications = buildList { + while (minId != null) { + val response = mastodonApi.notificationsWithAuth( + authHeader, + account.domain, + minId = minId + ) + if (!response.isSuccessful) break + + // Notifications are returned in the page in order, newest first, + // (https://github.com/mastodon/documentation/issues/1226), insert the + // new page at the head of the list. + response.body()?.let { addAll(0, it) } + + // Get the previous page, which will be chronologically newer + // notifications. If it doesn't exist this is null and the loop + // will exit. + val links = Links.from(response.headers()["link"]) + minId = links.prev + } + } + + // Save the newest notification ID in the marker. + notifications.firstOrNull()?.let { + val newMarkerId = notifications.first().id + Log.d(TAG, "updating notification marker for ${account.fullName} to: $newMarkerId") + mastodonApi.updateMarkersWithAuth( + auth = authHeader, + domain = account.domain, + notificationsLastReadId = newMarkerId + ) + account.notificationMarkerId = newMarkerId + accountManager.saveAccount(account) + } + + return notifications + } + + private suspend fun fetchMarker(authHeader: String, account: AccountEntity): Marker? { + return try { + val allMarkers = mastodonApi.markersWithAuth( + authHeader, + account.domain, + listOf("notifications") + ) + val notificationMarker = allMarkers["notifications"] + Log.d(TAG, "Fetched marker for ${account.fullName}: $notificationMarker") + notificationMarker + } catch (e: Exception) { + Log.e(TAG, "Failed to fetch marker", e) + null + } + } + + companion object { + private const val TAG = "NotificationFetcher" + + // There's a system limit on the maximum number of notifications an app + // can show, NotificationManagerService.MAX_PACKAGE_NOTIFICATIONS. Unfortunately + // that's not available to client code or via the NotificationManager API. + // The current value in the Android source code is 50, set 40 here to both + // be conservative, and allow some headroom for summary notifications. + private const val MAX_NOTIFICATIONS = 40 + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationHelper.java b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationHelper.java new file mode 100644 index 0000000..4a574db --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/NotificationHelper.java @@ -0,0 +1,866 @@ +/* Copyright 2018 Jeremiasz Nelz <remi6397(a)gmail.com> + * Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.systemnotifications; + +import static com.keylesspalace.tusky.BuildConfig.APPLICATION_ID; +import static com.keylesspalace.tusky.util.StatusParsingHelper.parseAsMastodonHtml; +import static com.keylesspalace.tusky.viewdata.PollViewDataKt.buildDescription; + +import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.os.Build; +import android.os.Bundle; +import android.provider.Settings; +import android.service.notification.StatusBarNotification; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; +import androidx.core.app.NotificationCompat; +import androidx.core.app.RemoteInput; +import androidx.core.app.TaskStackBuilder; +import androidx.work.Constraints; +import androidx.work.NetworkType; +import androidx.work.OneTimeWorkRequest; +import androidx.work.OutOfQuotaPolicy; +import androidx.work.PeriodicWorkRequest; +import androidx.work.WorkManager; +import androidx.work.WorkRequest; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.FutureTarget; +import com.keylesspalace.tusky.MainActivity; +import com.keylesspalace.tusky.R; +import com.keylesspalace.tusky.components.compose.ComposeActivity; +import com.keylesspalace.tusky.db.entity.AccountEntity; +import com.keylesspalace.tusky.db.AccountManager; +import com.keylesspalace.tusky.entity.Notification; +import com.keylesspalace.tusky.entity.Poll; +import com.keylesspalace.tusky.entity.PollOption; +import com.keylesspalace.tusky.entity.Status; +import com.keylesspalace.tusky.receiver.SendStatusBroadcastReceiver; +import com.keylesspalace.tusky.util.StringUtils; +import com.keylesspalace.tusky.viewdata.PollViewDataKt; +import com.keylesspalace.tusky.worker.NotificationWorker; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; + +public class NotificationHelper { + + /** ID of notification shown when fetching notifications */ + public static final int NOTIFICATION_ID_FETCH_NOTIFICATION = 0; + /** ID of notification shown when pruning the cache */ + public static final int NOTIFICATION_ID_PRUNE_CACHE = 1; + /** Dynamic notification IDs start here */ + private static int notificationId = NOTIFICATION_ID_PRUNE_CACHE + 1; + + private static final String TAG = "NotificationHelper"; + + public static final String REPLY_ACTION = "REPLY_ACTION"; + + public static final String KEY_REPLY = "KEY_REPLY"; + + public static final String KEY_SENDER_ACCOUNT_ID = "KEY_SENDER_ACCOUNT_ID"; + + public static final String KEY_SENDER_ACCOUNT_IDENTIFIER = "KEY_SENDER_ACCOUNT_IDENTIFIER"; + + public static final String KEY_SENDER_ACCOUNT_FULL_NAME = "KEY_SENDER_ACCOUNT_FULL_NAME"; + + public static final String KEY_SERVER_NOTIFICATION_ID = "KEY_SERVER_NOTIFICATION_ID"; + + public static final String KEY_CITED_STATUS_ID = "KEY_CITED_STATUS_ID"; + + public static final String KEY_VISIBILITY = "KEY_VISIBILITY"; + + public static final String KEY_SPOILER = "KEY_SPOILER"; + + public static final String KEY_MENTIONS = "KEY_MENTIONS"; + + /** + * notification channels used on Android O+ + **/ + public static final String CHANNEL_MENTION = "CHANNEL_MENTION"; + public static final String CHANNEL_FOLLOW = "CHANNEL_FOLLOW"; + public static final String CHANNEL_FOLLOW_REQUEST = "CHANNEL_FOLLOW_REQUEST"; + public static final String CHANNEL_BOOST = "CHANNEL_BOOST"; + public static final String CHANNEL_FAVOURITE = "CHANNEL_FAVOURITE"; + public static final String CHANNEL_POLL = "CHANNEL_POLL"; + public static final String CHANNEL_SUBSCRIPTIONS = "CHANNEL_SUBSCRIPTIONS"; + public static final String CHANNEL_SIGN_UP = "CHANNEL_SIGN_UP"; + public static final String CHANNEL_UPDATES = "CHANNEL_UPDATES"; + public static final String CHANNEL_REPORT = "CHANNEL_REPORT"; + public static final String CHANNEL_BACKGROUND_TASKS = "CHANNEL_BACKGROUND_TASKS"; + + /** + * WorkManager Tag + */ + private static final String NOTIFICATION_PULL_TAG = "pullNotifications"; + + /** Tag for the summary notification */ + private static final String GROUP_SUMMARY_TAG = APPLICATION_ID + ".notification.group_summary"; + + /** The name of the account that caused the notification, for use in a summary */ + private static final String EXTRA_ACCOUNT_NAME = APPLICATION_ID + ".notification.extra.account_name"; + + /** The notification's type (string representation of a Notification.Type) */ + private static final String EXTRA_NOTIFICATION_TYPE = APPLICATION_ID + ".notification.extra.notification_type"; + + /** + * Takes a given Mastodon notification and creates a new Android notification or updates the + * existing Android notification. + * <p> + * The Android notification has it's tag set to the Mastodon notification ID, and it's ID set + * to the ID of the account that received the notification. + * + * @param context to access application preferences and services + * @param body a new Mastodon notification + * @param account the account for which the notification should be shown + * @return the new notification + */ + @NonNull + public static android.app.Notification make(final @NonNull Context context, @NonNull NotificationManager notificationManager, @NonNull Notification body, @NonNull AccountEntity account, boolean isOnlyOneInGroup) { + body = body.rewriteToStatusTypeIfNeeded(account.getAccountId()); + String mastodonNotificationId = body.getId(); + int accountId = (int) account.getId(); + + // Check for an existing notification with this Mastodon Notification ID + android.app.Notification existingAndroidNotification = null; + StatusBarNotification[] activeNotifications = notificationManager.getActiveNotifications(); + for (StatusBarNotification androidNotification : activeNotifications) { + if (mastodonNotificationId.equals(androidNotification.getTag()) && accountId == androidNotification.getId()) { + existingAndroidNotification = androidNotification.getNotification(); + } + } + + // Notification group member + // ========================= + + notificationId++; + // Create the notification -- either create a new one, or use the existing one. + NotificationCompat.Builder builder; + if (existingAndroidNotification == null) { + builder = newAndroidNotification(context, body, account); + } else { + builder = new NotificationCompat.Builder(context, existingAndroidNotification); + } + + builder.setContentTitle(titleForType(context, body, account)) + .setContentText(bodyForType(body, context, account.getAlwaysOpenSpoiler())); + + if (body.getType() == Notification.Type.MENTION || body.getType() == Notification.Type.POLL) { + builder.setStyle(new NotificationCompat.BigTextStyle() + .bigText(bodyForType(body, context, account.getAlwaysOpenSpoiler()))); + } + + //load the avatar synchronously + Bitmap accountAvatar; + try { + FutureTarget<Bitmap> target = Glide.with(context) + .asBitmap() + .load(body.getAccount().getAvatar()) + .transform(new RoundedCorners(20)) + .submit(); + + accountAvatar = target.get(); + } catch (ExecutionException | InterruptedException e) { + Log.d(TAG, "error loading account avatar", e); + accountAvatar = BitmapFactory.decodeResource(context.getResources(), R.drawable.avatar_default); + } + + builder.setLargeIcon(accountAvatar); + + // Reply to mention action; RemoteInput is available from KitKat Watch, but buttons are available from Nougat + if (body.getType() == Notification.Type.MENTION) { + RemoteInput replyRemoteInput = new RemoteInput.Builder(KEY_REPLY) + .setLabel(context.getString(R.string.label_quick_reply)) + .build(); + + PendingIntent quickReplyPendingIntent = getStatusReplyIntent(context, body, account); + + NotificationCompat.Action quickReplyAction = + new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, + context.getString(R.string.action_quick_reply), + quickReplyPendingIntent) + .addRemoteInput(replyRemoteInput) + .build(); + + builder.addAction(quickReplyAction); + + PendingIntent composeIntent = getStatusComposeIntent(context, body, account); + + NotificationCompat.Action composeAction = + new NotificationCompat.Action.Builder(R.drawable.ic_reply_24dp, + context.getString(R.string.action_compose_shortcut), + composeIntent) + .setShowsUserInterface(true) + .build(); + + builder.addAction(composeAction); + } + + builder.setSubText(account.getFullName()); + builder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE); + builder.setCategory(NotificationCompat.CATEGORY_SOCIAL); + builder.setOnlyAlertOnce(true); + + Bundle extras = new Bundle(); + // Add the sending account's name, so it can be used when summarising this notification + extras.putString(EXTRA_ACCOUNT_NAME, body.getAccount().getName()); + extras.putString(EXTRA_NOTIFICATION_TYPE, body.getType().name()); + builder.addExtras(extras); + + // Only alert for the first notification of a batch to avoid multiple alerts at once + if(!isOnlyOneInGroup) { + builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY); + } + + return builder.build(); + } + + /** + * Updates the summary notifications for each notification group. + * <p> + * Notifications are sent to channels. Within each channel they may be grouped, and the group + * may have a summary. + * <p> + * Tusky uses N notification channels for each account, each channel corresponds to a type + * of notification (follow, reblog, mention, etc). Therefore each channel also has exactly + * 0 or 1 summary notifications along with its regular notifications. + * <p> + * The group key is the same as the channel ID. + * <p> + * Regnerates the summary notifications for all active Tusky notifications for `account`. + * This may delete the summary notification if there are no active notifications for that + * account in a group. + * + * @see <a href="https://developer.android.com/develop/ui/views/notifications/group">Create a + * notification group</a> + * @param context to access application preferences and services + * @param notificationManager the system's NotificationManager + * @param account the account for which the notification should be shown + */ + public static void updateSummaryNotifications(@NonNull Context context, @NonNull NotificationManager notificationManager, @NonNull AccountEntity account) { + // Map from the channel ID to a list of notifications in that channel. Those are the + // notifications that will be summarised. + Map<String, List<StatusBarNotification>> channelGroups = new HashMap<>(); + int accountId = (int) account.getId(); + + // Initialise the map with all channel IDs. + for (Notification.Type ty : Notification.Type.getEntries()) { + channelGroups.put(getChannelId(account, ty), new ArrayList<>()); + } + + // Fetch all existing notifications. Add them to the map, ignoring notifications that: + // - belong to a different account + // - are summary notifications + for (StatusBarNotification sn : notificationManager.getActiveNotifications()) { + if (sn.getId() != accountId) continue; + + String channelId = sn.getNotification().getGroup(); + String summaryTag = GROUP_SUMMARY_TAG + "." + channelId; + if (summaryTag.equals(sn.getTag())) continue; + + // TODO: API 26 supports getting the channel ID directly (sn.getNotification().getChannelId()). + // This works here because the channelId and the groupKey are the same. + List<StatusBarNotification> members = channelGroups.get(channelId); + if (members == null) { // can't happen, but just in case... + Log.e(TAG, "members == null for channel ID " + channelId); + continue; + } + members.add(sn); + } + + // Create, update, or cancel the summary notifications for each group. + for (Map.Entry<String, List<StatusBarNotification>> channelGroup : channelGroups.entrySet()) { + String channelId = channelGroup.getKey(); + List<StatusBarNotification> members = channelGroup.getValue(); + String summaryTag = GROUP_SUMMARY_TAG + "." + channelId; + + // If there are 0-1 notifications in this group then the additional summary + // notification is not needed and can be cancelled. + if (members.size() <= 1) { + notificationManager.cancel(summaryTag, accountId); + continue; + } + + // Create a notification that summarises the other notifications in this group + + // All notifications in this group have the same type, so get it from the first. + String typeName = members.get(0).getNotification().extras.getString(EXTRA_NOTIFICATION_TYPE, Notification.Type.UNKNOWN.name()); + Notification.Type notificationType = Notification.Type.valueOf(typeName); + + Intent summaryResultIntent = MainActivity.openNotificationIntent(context, accountId, notificationType); + + TaskStackBuilder summaryStackBuilder = TaskStackBuilder.create(context); + summaryStackBuilder.addParentStack(MainActivity.class); + summaryStackBuilder.addNextIntent(summaryResultIntent); + + PendingIntent summaryResultPendingIntent = summaryStackBuilder.getPendingIntent((int) (notificationId + account.getId() * 10000), + pendingIntentFlags(false)); + + String title = context.getResources().getQuantityString(R.plurals.notification_title_summary, members.size(), members.size()); + String text = joinNames(context, members); + + NotificationCompat.Builder summaryBuilder = new NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.ic_notify) + .setContentIntent(summaryResultPendingIntent) + .setColor(context.getColor(R.color.notification_color)) + .setAutoCancel(true) + .setShortcutId(Long.toString(account.getId())) + .setDefaults(0) // So it doesn't ring twice, notify only in Target callback + .setContentTitle(title) + .setContentText(text) + .setSubText(account.getFullName()) + .setVisibility(NotificationCompat.VISIBILITY_PRIVATE) + .setCategory(NotificationCompat.CATEGORY_SOCIAL) + .setOnlyAlertOnce(true) + .setGroup(channelId) + .setGroupSummary(true); + + setSoundVibrationLight(account, summaryBuilder); + + // TODO: Use the batch notification API available in NotificationManagerCompat + // 1.11 and up (https://developer.android.com/jetpack/androidx/releases/core#1.11.0-alpha01) + // when it is released. + notificationManager.notify(summaryTag, accountId, summaryBuilder.build()); + + // Android will rate limit / drop notifications if they're posted too + // quickly. There is no indication to the user that this happened. + // See https://github.com/tuskyapp/Tusky/pull/3626#discussion_r1192963664 + try { Thread.sleep(1000); } catch (InterruptedException ignored) { } + } + } + + + private static NotificationCompat.Builder newAndroidNotification(Context context, Notification body, AccountEntity account) { + + Intent eventResultIntent = MainActivity.openNotificationIntent(context, account.getId(), body.getType()); + + TaskStackBuilder eventStackBuilder = TaskStackBuilder.create(context); + eventStackBuilder.addParentStack(MainActivity.class); + eventStackBuilder.addNextIntent(eventResultIntent); + + PendingIntent eventResultPendingIntent = eventStackBuilder.getPendingIntent((int) account.getId(), + pendingIntentFlags(false)); + + String channelId = getChannelId(account, body); + assert channelId != null; + + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, channelId) + .setSmallIcon(R.drawable.ic_notify) + .setContentIntent(eventResultPendingIntent) + .setColor(context.getColor(R.color.notification_color)) + .setGroup(channelId) + .setAutoCancel(true) + .setShortcutId(Long.toString(account.getId())) + .setDefaults(0); // So it doesn't ring twice, notify only in Target callback + + setSoundVibrationLight(account, builder); + + return builder; + } + + private static PendingIntent getStatusReplyIntent(Context context, Notification body, AccountEntity account) { + Status status = body.getStatus(); + + String inReplyToId = status.getId(); + Status actionableStatus = status.getActionableStatus(); + Status.Visibility replyVisibility = actionableStatus.getVisibility(); + String contentWarning = actionableStatus.getSpoilerText(); + List<Status.Mention> mentions = actionableStatus.getMentions(); + List<String> mentionedUsernames = new ArrayList<>(); + mentionedUsernames.add(actionableStatus.getAccount().getUsername()); + for (Status.Mention mention : mentions) { + mentionedUsernames.add(mention.getUsername()); + } + mentionedUsernames.removeAll(Collections.singleton(account.getUsername())); + mentionedUsernames = new ArrayList<>(new LinkedHashSet<>(mentionedUsernames)); + + Intent replyIntent = new Intent(context, SendStatusBroadcastReceiver.class) + .setAction(REPLY_ACTION) + .putExtra(KEY_SENDER_ACCOUNT_ID, account.getId()) + .putExtra(KEY_SENDER_ACCOUNT_IDENTIFIER, account.getIdentifier()) + .putExtra(KEY_SENDER_ACCOUNT_FULL_NAME, account.getFullName()) + .putExtra(KEY_SERVER_NOTIFICATION_ID, body.getId()) + .putExtra(KEY_CITED_STATUS_ID, inReplyToId) + .putExtra(KEY_VISIBILITY, replyVisibility) + .putExtra(KEY_SPOILER, contentWarning) + .putExtra(KEY_MENTIONS, mentionedUsernames.toArray(new String[0])); + + return PendingIntent.getBroadcast(context.getApplicationContext(), + notificationId, + replyIntent, + pendingIntentFlags(true)); + } + + private static PendingIntent getStatusComposeIntent(Context context, Notification body, AccountEntity account) { + Status status = body.getStatus(); + + String citedLocalAuthor = status.getAccount().getLocalUsername(); + String citedText = parseAsMastodonHtml(status.getContent()).toString(); + String inReplyToId = status.getId(); + Status actionableStatus = status.getActionableStatus(); + Status.Visibility replyVisibility = actionableStatus.getVisibility(); + String contentWarning = actionableStatus.getSpoilerText(); + List<Status.Mention> mentions = actionableStatus.getMentions(); + Set<String> mentionedUsernames = new LinkedHashSet<>(); + mentionedUsernames.add(actionableStatus.getAccount().getUsername()); + for (Status.Mention mention : mentions) { + String mentionedUsername = mention.getUsername(); + if (!mentionedUsername.equals(account.getUsername())) { + mentionedUsernames.add(mention.getUsername()); + } + } + + ComposeActivity.ComposeOptions composeOptions = new ComposeActivity.ComposeOptions(); + composeOptions.setInReplyToId(inReplyToId); + composeOptions.setReplyVisibility(replyVisibility); + composeOptions.setContentWarning(contentWarning); + composeOptions.setReplyingStatusAuthor(citedLocalAuthor); + composeOptions.setReplyingStatusContent(citedText); + composeOptions.setMentionedUsernames(mentionedUsernames); + composeOptions.setModifiedInitialState(true); + composeOptions.setLanguage(actionableStatus.getLanguage()); + composeOptions.setKind(ComposeActivity.ComposeKind.NEW); + + Intent composeIntent = MainActivity.composeIntent(context, composeOptions, account.getId(), body.getId(), (int)account.getId()); + + // make sure a new instance of MainActivity is started and old ones get destroyed + composeIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + + return PendingIntent.getActivity(context.getApplicationContext(), + notificationId, + composeIntent, + pendingIntentFlags(false)); + } + + /** + * Creates a notification channel for notifications for background work that should not + * disturb the user. + * + * @param context context + */ + public static void createWorkerNotificationChannel(@NonNull Context context) { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return; + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + NotificationChannel channel = new NotificationChannel( + CHANNEL_BACKGROUND_TASKS, + context.getString(R.string.notification_listenable_worker_name), + NotificationManager.IMPORTANCE_NONE + ); + + channel.setDescription(context.getString(R.string.notification_listenable_worker_description)); + channel.enableLights(false); + channel.enableVibration(false); + channel.setShowBadge(false); + + notificationManager.createNotificationChannel(channel); + } + + /** + * Creates a notification for a background worker. + * + * @param context context + * @param titleResource String resource to use as the notification's title + * @return the notification + */ + @NonNull + public static android.app.Notification createWorkerNotification(@NonNull Context context, @StringRes int titleResource) { + String title = context.getString(titleResource); + return new NotificationCompat.Builder(context, CHANNEL_BACKGROUND_TASKS) + .setContentTitle(title) + .setTicker(title) + .setSmallIcon(R.drawable.ic_notify) + .setOngoing(true) + .build(); + } + + public static void createNotificationChannelsForAccount(@NonNull AccountEntity account, @NonNull Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + String[] channelIds = new String[]{ + CHANNEL_MENTION + account.getIdentifier(), + CHANNEL_FOLLOW + account.getIdentifier(), + CHANNEL_FOLLOW_REQUEST + account.getIdentifier(), + CHANNEL_BOOST + account.getIdentifier(), + CHANNEL_FAVOURITE + account.getIdentifier(), + CHANNEL_POLL + account.getIdentifier(), + CHANNEL_SUBSCRIPTIONS + account.getIdentifier(), + CHANNEL_SIGN_UP + account.getIdentifier(), + CHANNEL_UPDATES + account.getIdentifier(), + CHANNEL_REPORT + account.getIdentifier(), + }; + int[] channelNames = { + R.string.notification_mention_name, + R.string.notification_follow_name, + R.string.notification_follow_request_name, + R.string.notification_boost_name, + R.string.notification_favourite_name, + R.string.notification_poll_name, + R.string.notification_subscription_name, + R.string.notification_sign_up_name, + R.string.notification_update_name, + R.string.notification_report_name, + }; + int[] channelDescriptions = { + R.string.notification_mention_descriptions, + R.string.notification_follow_description, + R.string.notification_follow_request_description, + R.string.notification_boost_description, + R.string.notification_favourite_description, + R.string.notification_poll_description, + R.string.notification_subscription_description, + R.string.notification_sign_up_description, + R.string.notification_update_description, + R.string.notification_report_description, + }; + + List<NotificationChannel> channels = new ArrayList<>(6); + + NotificationChannelGroup channelGroup = new NotificationChannelGroup(account.getIdentifier(), account.getFullName()); + + notificationManager.createNotificationChannelGroup(channelGroup); + + for (int i = 0; i < channelIds.length; i++) { + String id = channelIds[i]; + String name = context.getString(channelNames[i]); + String description = context.getString(channelDescriptions[i]); + int importance = NotificationManager.IMPORTANCE_DEFAULT; + NotificationChannel channel = new NotificationChannel(id, name, importance); + + channel.setDescription(description); + channel.enableLights(true); + channel.setLightColor(0xFF2B90D9); + channel.enableVibration(true); + channel.setShowBadge(true); + channel.setGroup(account.getIdentifier()); + channels.add(channel); + } + + notificationManager.createNotificationChannels(channels); + } + } + + public static void deleteNotificationChannelsForAccount(@NonNull AccountEntity account, @NonNull Context context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + notificationManager.deleteNotificationChannelGroup(account.getIdentifier()); + } + } + + public static boolean areNotificationsEnabled(@NonNull Context context, @NonNull AccountManager accountManager) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + + // on Android >= O, notifications are enabled, if at least one channel is enabled + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + if (notificationManager.areNotificationsEnabled()) { + for (NotificationChannel channel : notificationManager.getNotificationChannels()) { + if (channel != null && channel.getImportance() > NotificationManager.IMPORTANCE_NONE) { + Log.d(TAG, "NotificationsEnabled"); + return true; + } + } + } + Log.d(TAG, "NotificationsDisabled"); + + return false; + + } else { + // on Android < O, notifications are enabled, if at least one account has notification enabled + return accountManager.areNotificationsEnabled(); + } + + } + + public static void enablePullNotifications(@NonNull Context context) { + WorkManager workManager = WorkManager.getInstance(context); + workManager.cancelAllWorkByTag(NOTIFICATION_PULL_TAG); + + // Periodic work requests are supposed to start running soon after being enqueued. In + // practice that may not be soon enough, so create and enqueue an expedited one-time + // request to get new notifications immediately. + WorkRequest fetchNotifications = new OneTimeWorkRequest.Builder(NotificationWorker.class) + .setExpedited(OutOfQuotaPolicy.DROP_WORK_REQUEST) + .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) + .build(); + workManager.enqueue(fetchNotifications); + + WorkRequest workRequest = new PeriodicWorkRequest.Builder( + NotificationWorker.class, + PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS, + PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS + ) + .addTag(NOTIFICATION_PULL_TAG) + .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) + .setInitialDelay(5, TimeUnit.MINUTES) + .build(); + + workManager.enqueue(workRequest); + + Log.d(TAG, "enabled notification checks with "+ PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS + "ms interval"); + } + + public static void disablePullNotifications(@NonNull Context context) { + WorkManager.getInstance(context).cancelAllWorkByTag(NOTIFICATION_PULL_TAG); + Log.d(TAG, "disabled notification checks"); + } + + public static void clearNotificationsForAccount(@NonNull Context context, @NonNull AccountEntity account) { + int accountId = (int) account.getId(); + + NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + for (StatusBarNotification androidNotification : notificationManager.getActiveNotifications()) { + if (accountId == androidNotification.getId()) { + notificationManager.cancel(androidNotification.getTag(), androidNotification.getId()); + } + } + } + + public static boolean filterNotification(@NonNull NotificationManager notificationManager, @NonNull AccountEntity account, @NonNull Notification notification) { + return filterNotification(notificationManager, account, notification.getType()); + } + + public static boolean filterNotification(@NonNull NotificationManager notificationManager, @NonNull AccountEntity account, @NonNull Notification.Type type) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + String channelId = getChannelId(account, type); + if(channelId == null) { + // unknown notificationtype + return false; + } + NotificationChannel channel = notificationManager.getNotificationChannel(channelId); + return channel != null && channel.getImportance() > NotificationManager.IMPORTANCE_NONE; + } + + switch (type) { + case MENTION: + return account.getNotificationsMentioned(); + case STATUS: + return account.getNotificationsSubscriptions(); + case FOLLOW: + return account.getNotificationsFollowed(); + case FOLLOW_REQUEST: + return account.getNotificationsFollowRequested(); + case REBLOG: + return account.getNotificationsReblogged(); + case FAVOURITE: + return account.getNotificationsFavorited(); + case POLL: + return account.getNotificationsPolls(); + case SIGN_UP: + return account.getNotificationsSignUps(); + case UPDATE: + return account.getNotificationsUpdates(); + case REPORT: + return account.getNotificationsReports(); + default: + return false; + } + } + + @Nullable + private static String getChannelId(AccountEntity account, Notification notification) { + return getChannelId(account, notification.getType()); + } + + @Nullable + private static String getChannelId(AccountEntity account, Notification.Type type) { + switch (type) { + case MENTION: + return CHANNEL_MENTION + account.getIdentifier(); + case STATUS: + return CHANNEL_SUBSCRIPTIONS + account.getIdentifier(); + case FOLLOW: + return CHANNEL_FOLLOW + account.getIdentifier(); + case FOLLOW_REQUEST: + return CHANNEL_FOLLOW_REQUEST + account.getIdentifier(); + case REBLOG: + return CHANNEL_BOOST + account.getIdentifier(); + case FAVOURITE: + return CHANNEL_FAVOURITE + account.getIdentifier(); + case POLL: + return CHANNEL_POLL + account.getIdentifier(); + case SIGN_UP: + return CHANNEL_SIGN_UP + account.getIdentifier(); + case UPDATE: + return CHANNEL_UPDATES + account.getIdentifier(); + case REPORT: + return CHANNEL_REPORT + account.getIdentifier(); + default: + return null; + } + + } + + private static void setSoundVibrationLight(AccountEntity account, NotificationCompat.Builder builder) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + return; //do nothing on Android O or newer, the system uses the channel settings anyway + } + + if (account.getNotificationSound()) { + builder.setSound(Settings.System.DEFAULT_NOTIFICATION_URI); + } + + if (account.getNotificationVibration()) { + builder.setVibrate(new long[]{500, 500}); + } + + if (account.getNotificationLight()) { + builder.setLights(0xFF2B90D9, 300, 1000); + } + } + + private static String wrapItemAt(StatusBarNotification notification) { + return StringUtils.unicodeWrap(notification.getNotification().extras.getString(EXTRA_ACCOUNT_NAME));//getAccount().getName()); + } + + @Nullable + private static String joinNames(Context context, List<StatusBarNotification> notifications) { + if (notifications.size() > 3) { + int length = notifications.size(); + //notifications.get(0).getNotification().extras.getString(EXTRA_ACCOUNT_NAME); + return String.format(context.getString(R.string.notification_summary_large), + wrapItemAt(notifications.get(length - 1)), + wrapItemAt(notifications.get(length - 2)), + wrapItemAt(notifications.get(length - 3)), + length - 3); + } else if (notifications.size() == 3) { + return String.format(context.getString(R.string.notification_summary_medium), + wrapItemAt(notifications.get(2)), + wrapItemAt(notifications.get(1)), + wrapItemAt(notifications.get(0))); + } else if (notifications.size() == 2) { + return String.format(context.getString(R.string.notification_summary_small), + wrapItemAt(notifications.get(1)), + wrapItemAt(notifications.get(0))); + } + + return null; + } + + @Nullable + private static String titleForType(Context context, Notification notification, AccountEntity account) { + String accountName = StringUtils.unicodeWrap(notification.getAccount().getName()); + switch (notification.getType()) { + case MENTION: + return String.format(context.getString(R.string.notification_mention_format), + accountName); + case STATUS: + return String.format(context.getString(R.string.notification_subscription_format), + accountName); + case FOLLOW: + return String.format(context.getString(R.string.notification_follow_format), + accountName); + case FOLLOW_REQUEST: + return String.format(context.getString(R.string.notification_follow_request_format), + accountName); + case FAVOURITE: + return String.format(context.getString(R.string.notification_favourite_format), + accountName); + case REBLOG: + return String.format(context.getString(R.string.notification_reblog_format), + accountName); + case POLL: + if(notification.getStatus().getAccount().getId().equals(account.getAccountId())) { + return context.getString(R.string.poll_ended_created); + } else { + return context.getString(R.string.poll_ended_voted); + } + case SIGN_UP: + return String.format(context.getString(R.string.notification_sign_up_format), accountName); + case UPDATE: + return String.format(context.getString(R.string.notification_update_format), accountName); + case REPORT: + return context.getString(R.string.notification_report_format, account.getDomain()); + } + return null; + } + + private static String bodyForType(Notification notification, Context context, Boolean alwaysOpenSpoiler) { + switch (notification.getType()) { + case FOLLOW: + case FOLLOW_REQUEST: + case SIGN_UP: + return "@" + notification.getAccount().getUsername(); + case MENTION: + case FAVOURITE: + case REBLOG: + case STATUS: + if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText()) && !alwaysOpenSpoiler) { + return notification.getStatus().getSpoilerText(); + } else { + return parseAsMastodonHtml(notification.getStatus().getContent()).toString(); + } + case POLL: + if (!TextUtils.isEmpty(notification.getStatus().getSpoilerText()) && !alwaysOpenSpoiler) { + return notification.getStatus().getSpoilerText(); + } else { + StringBuilder builder = new StringBuilder(parseAsMastodonHtml(notification.getStatus().getContent())); + builder.append('\n'); + Poll poll = notification.getStatus().getPoll(); + List<PollOption> options = poll.getOptions(); + for(int i = 0; i < options.size(); ++i) { + PollOption option = options.get(i); + builder.append(buildDescription(option.getTitle(), + PollViewDataKt.calculatePercent(option.getVotesCount(), poll.getVotersCount(), poll.getVotesCount()), + poll.getOwnVotes().contains(i), + context)); + builder.append('\n'); + } + return builder.toString(); + } + case REPORT: + return context.getString( + R.string.notification_header_report_format, + StringUtils.unicodeWrap(notification.getAccount().getName()), + StringUtils.unicodeWrap(notification.getReport().getTargetAccount().getName()) + ); + } + return null; + } + + public static int pendingIntentFlags(boolean mutable) { + if (mutable) { + return PendingIntent.FLAG_UPDATE_CURRENT | (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0); + } else { + return PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE; + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/PushNotificationHelper.kt b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/PushNotificationHelper.kt new file mode 100644 index 0000000..4e7c1a6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/systemnotifications/PushNotificationHelper.kt @@ -0,0 +1,262 @@ +/* Copyright 2022 Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +@file:JvmName("PushNotificationHelper") + +package com.keylesspalace.tusky.components.systemnotifications + +import android.app.NotificationManager +import android.content.Context +import android.os.Build +import android.util.Log +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.preference.PreferenceManager +import at.connyduck.calladapter.networkresult.onFailure +import at.connyduck.calladapter.networkresult.onSuccess +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.login.LoginActivity +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.CryptoUtil +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.unifiedpush.android.connector.UnifiedPush + +private const val TAG = "PushNotificationHelper" + +private const val KEY_MIGRATION_NOTICE_DISMISSED = "migration_notice_dismissed" + +private fun anyAccountNeedsMigration(accountManager: AccountManager): Boolean = + accountManager.accounts.any(::accountNeedsMigration) + +private fun accountNeedsMigration(account: AccountEntity): Boolean = + !account.oauthScopes.contains("push") + +fun currentAccountNeedsMigration(accountManager: AccountManager): Boolean = + accountManager.activeAccount?.let(::accountNeedsMigration) ?: false + +fun showMigrationNoticeIfNecessary( + context: Context, + parent: View, + anchorView: View?, + accountManager: AccountManager +) { + // No point showing anything if we cannot enable it + if (!isUnifiedPushAvailable(context)) return + if (!anyAccountNeedsMigration(accountManager)) return + + val pm = PreferenceManager.getDefaultSharedPreferences(context) + if (pm.getBoolean(KEY_MIGRATION_NOTICE_DISMISSED, false)) return + + Snackbar.make(parent, R.string.tips_push_notification_migration, Snackbar.LENGTH_INDEFINITE) + .setAnchorView(anchorView) + .setAction( + R.string.action_details + ) { showMigrationExplanationDialog(context, accountManager) } + .show() +} + +private fun showMigrationExplanationDialog(context: Context, accountManager: AccountManager) { + AlertDialog.Builder(context).apply { + if (currentAccountNeedsMigration(accountManager)) { + setMessage(R.string.dialog_push_notification_migration) + setPositiveButton(R.string.title_migration_relogin) { _, _ -> + context.startActivity( + LoginActivity.getIntent(context, LoginActivity.MODE_MIGRATION) + ) + } + } else { + setMessage(R.string.dialog_push_notification_migration_other_accounts) + } + setNegativeButton(R.string.action_dismiss) { dialog, _ -> + val pm = PreferenceManager.getDefaultSharedPreferences(context) + pm.edit().putBoolean(KEY_MIGRATION_NOTICE_DISMISSED, true).apply() + dialog.dismiss() + } + show() + } +} + +private suspend fun enableUnifiedPushNotificationsForAccount( + context: Context, + api: MastodonApi, + accountManager: AccountManager, + account: AccountEntity +) { + if (isUnifiedPushNotificationEnabledForAccount(account)) { + // Already registered, update the subscription to match notification settings + updateUnifiedPushSubscription(context, api, accountManager, account) + } else { + UnifiedPush.registerAppWithDialog( + context, + account.id.toString(), + features = arrayListOf(UnifiedPush.FEATURE_BYTES_MESSAGE) + ) + } +} + +fun disableUnifiedPushNotificationsForAccount(context: Context, account: AccountEntity) { + if (!isUnifiedPushNotificationEnabledForAccount(account)) { + // Not registered + return + } + + UnifiedPush.unregisterApp(context, account.id.toString()) +} + +fun isUnifiedPushNotificationEnabledForAccount(account: AccountEntity): Boolean = + account.unifiedPushUrl.isNotEmpty() + +private fun isUnifiedPushAvailable(context: Context): Boolean = + UnifiedPush.getDistributors(context).isNotEmpty() + +fun canEnablePushNotifications(context: Context, accountManager: AccountManager): Boolean = + isUnifiedPushAvailable(context) && !anyAccountNeedsMigration(accountManager) + +suspend fun enablePushNotificationsWithFallback( + context: Context, + api: MastodonApi, + accountManager: AccountManager +) { + if (!canEnablePushNotifications(context, accountManager)) { + // No UP distributors + NotificationHelper.enablePullNotifications(context) + return + } + + val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + accountManager.accounts.forEach { + val notificationGroupEnabled = Build.VERSION.SDK_INT < 28 || + nm.getNotificationChannelGroup(it.identifier)?.isBlocked == false + val shouldEnable = it.notificationsEnabled && notificationGroupEnabled + + if (shouldEnable) { + enableUnifiedPushNotificationsForAccount(context, api, accountManager, it) + } else { + disableUnifiedPushNotificationsForAccount(context, it) + } + } +} + +private fun disablePushNotifications(context: Context, accountManager: AccountManager) { + accountManager.accounts.forEach { + disableUnifiedPushNotificationsForAccount(context, it) + } +} + +fun disableAllNotifications(context: Context, accountManager: AccountManager) { + disablePushNotifications(context, accountManager) + NotificationHelper.disablePullNotifications(context) +} + +private fun buildSubscriptionData(context: Context, account: AccountEntity): Map<String, Boolean> = + buildMap { + val notificationManager = context.getSystemService( + Context.NOTIFICATION_SERVICE + ) as NotificationManager + Notification.Type.visibleTypes.forEach { + put( + "data[alerts][${it.presentation}]", + NotificationHelper.filterNotification(notificationManager, account, it) + ) + } + } + +// Called by UnifiedPush callback +suspend fun registerUnifiedPushEndpoint( + context: Context, + api: MastodonApi, + accountManager: AccountManager, + account: AccountEntity, + endpoint: String +) = withContext(Dispatchers.IO) { + // Generate a prime256v1 key pair for WebPush + // Decryption is unimplemented for now, since Mastodon uses an old WebPush + // standard which does not send needed information for decryption in the payload + // This makes it not directly compatible with UnifiedPush + // As of now, we use it purely as a way to trigger a pull + val keyPair = CryptoUtil.generateECKeyPair(CryptoUtil.CURVE_PRIME256_V1) + val auth = CryptoUtil.secureRandomBytesEncoded(16) + + api.subscribePushNotifications( + "Bearer ${account.accessToken}", + account.domain, + endpoint, + keyPair.pubkey, + auth, + buildSubscriptionData(context, account) + ).onFailure { throwable -> + Log.w(TAG, "Error setting push endpoint for account ${account.id}", throwable) + disableUnifiedPushNotificationsForAccount(context, account) + }.onSuccess { + Log.d(TAG, "UnifiedPush registration succeeded for account ${account.id}") + + account.pushPubKey = keyPair.pubkey + account.pushPrivKey = keyPair.privKey + account.pushAuth = auth + account.pushServerKey = it.serverKey + account.unifiedPushUrl = endpoint + accountManager.saveAccount(account) + } +} + +// Synchronize the enabled / disabled state of notifications with server-side subscription +suspend fun updateUnifiedPushSubscription( + context: Context, + api: MastodonApi, + accountManager: AccountManager, + account: AccountEntity +) { + withContext(Dispatchers.IO) { + api.updatePushNotificationSubscription( + "Bearer ${account.accessToken}", + account.domain, + buildSubscriptionData(context, account) + ).onSuccess { + Log.d(TAG, "UnifiedPush subscription updated for account ${account.id}") + + account.pushServerKey = it.serverKey + accountManager.saveAccount(account) + } + } +} + +suspend fun unregisterUnifiedPushEndpoint( + api: MastodonApi, + accountManager: AccountManager, + account: AccountEntity +) { + withContext(Dispatchers.IO) { + api.unsubscribePushNotifications("Bearer ${account.accessToken}", account.domain) + .onFailure { throwable -> + Log.w(TAG, "Error unregistering push endpoint for account " + account.id, throwable) + } + .onSuccess { + Log.d(TAG, "UnifiedPush unregistration succeeded for account " + account.id) + // Clear the URL in database + account.unifiedPushUrl = "" + account.pushServerKey = "" + account.pushAuth = "" + account.pushPrivKey = "" + account.pushPubKey = "" + accountManager.saveAccount(account) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt new file mode 100644 index 0000000..fb09d03 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineFragment.kt @@ -0,0 +1,682 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.timeline + +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.accessibility.AccessibilityManager +import androidx.core.content.getSystemService +import androidx.core.view.MenuProvider +import androidx.core.view.updatePadding +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.paging.LoadState +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import at.connyduck.calladapter.networkresult.onFailure +import at.connyduck.sparkbutton.helpers.Utils +import com.google.android.material.color.MaterialColors +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.appstore.StatusComposedEvent +import com.keylesspalace.tusky.components.accountlist.AccountListActivity +import com.keylesspalace.tusky.components.accountlist.AccountListActivity.Companion.newIntent +import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder +import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineViewModel +import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel +import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel +import com.keylesspalace.tusky.databinding.FragmentTimelineBinding +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.fragment.SFragment +import com.keylesspalace.tusky.interfaces.ActionButtonActivity +import com.keylesspalace.tusky.interfaces.RefreshableFragment +import com.keylesspalace.tusky.interfaces.ReselectableFragment +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import com.keylesspalace.tusky.util.unsafeLazy +import com.keylesspalace.tusky.util.updateRelativeTimePeriodically +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.keylesspalace.tusky.viewdata.TranslationViewData +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class TimelineFragment : + SFragment(R.layout.fragment_timeline), + OnRefreshListener, + StatusActionListener, + ReselectableFragment, + RefreshableFragment, + MenuProvider { + + @Inject + lateinit var eventHub: EventHub + + @Inject + lateinit var preferences: SharedPreferences + + private val viewModel: TimelineViewModel by unsafeLazy { + val viewModelProvider = ViewModelProvider(viewModelStore, defaultViewModelProviderFactory, defaultViewModelCreationExtras) + if (kind == TimelineViewModel.Kind.HOME) { + viewModelProvider[CachedTimelineViewModel::class.java] + } else { + viewModelProvider[NetworkTimelineViewModel::class.java] + } + } + + private val binding by viewBinding(FragmentTimelineBinding::bind) + + private lateinit var kind: TimelineViewModel.Kind + + private var adapter: TimelinePagingAdapter? = null + + private var isSwipeToRefreshEnabled = true + + /** + * Adapter position of the placeholder that was most recently clicked to "Load more". If null + * then there is no active "Load more" operation + */ + private var loadMorePosition: Int? = null + + /** ID of the status immediately below the most recent "Load more" placeholder click */ + // The Paging library assumes that the user will be scrolling down a list of items, + // and if new items are loaded but not visible then it's reasonable to scroll to the top + // of the inserted items. It does not seem to be possible to disable that behaviour. + // + // That behaviour should depend on the user's preferred reading order. If they prefer to + // read oldest first then the list should be scrolled to the bottom of the freshly + // inserted statuses. + // + // To do this: + // + // 1. When "Load more" is clicked (onLoadMore()): + // a. Remember the adapter position of the "Load more" item in loadMorePosition + // b. Remember the ID of the status immediately below the "Load more" item in + // statusIdBelowLoadMore + // 2. After the new items have been inserted, search the adapter for the position of the + // status with id == statusIdBelowLoadMore. + // 3. If this position is still visible on screen then do nothing, otherwise, scroll the view + // so that the status is visible. + // + // The user can then scroll up to read the new statuses. + private var statusIdBelowLoadMore: String? = null + + /** The user's preferred reading order */ + private lateinit var readingOrder: ReadingOrder + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val arguments = requireArguments() + kind = TimelineViewModel.Kind.valueOf(arguments.getString(KIND_ARG)!!) + val id: String? = if (kind == TimelineViewModel.Kind.USER || + kind == TimelineViewModel.Kind.USER_PINNED || + kind == TimelineViewModel.Kind.USER_WITH_REPLIES || + kind == TimelineViewModel.Kind.LIST + ) { + arguments.getString(ID_ARG)!! + } else { + null + } + + val tags = if (kind == TimelineViewModel.Kind.TAG) { + arguments.getStringArrayList(HASHTAGS_ARG)!! + } else { + listOf() + } + viewModel.init( + kind, + id, + tags + ) + + isSwipeToRefreshEnabled = arguments.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) + + readingOrder = ReadingOrder.from(preferences.getString(PrefKeys.READING_ORDER, null)) + } + + private fun createAdapter(): TimelinePagingAdapter { + val statusDisplayOptions = StatusDisplayOptions( + animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled, + useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false), + showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true), + useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true), + cardViewMode = if (preferences.getBoolean( + PrefKeys.SHOW_CARDS_IN_TIMELINES, + false + ) + ) { + CardViewMode.INDENTED + } else { + CardViewMode.NONE + }, + confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true), + confirmFavourites = preferences.getBoolean(PrefKeys.CONFIRM_FAVOURITES, false), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), + showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false), + showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia, + openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler + ) + return TimelinePagingAdapter( + statusDisplayOptions, + this + ) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) + + val adapter = createAdapter() + this.adapter = adapter + + setupSwipeRefreshLayout() + setupRecyclerView(adapter) + + adapter.addLoadStateListener { loadState -> + if (loadState.refresh != LoadState.Loading && loadState.source.refresh != LoadState.Loading) { + binding.swipeRefreshLayout.isRefreshing = false + } + + binding.statusView.hide() + binding.progressBar.hide() + + if (adapter.itemCount == 0) { + when (loadState.refresh) { + is LoadState.NotLoading -> { + if (loadState.append is LoadState.NotLoading && loadState.source.refresh is LoadState.NotLoading) { + binding.statusView.show() + binding.statusView.setup( + R.drawable.elephant_friend_empty, + R.string.message_empty + ) + if (kind == TimelineViewModel.Kind.HOME) { + binding.statusView.showHelp(R.string.help_empty_home) + } + } + } + + is LoadState.Error -> { + binding.statusView.show() + binding.statusView.setup( + (loadState.refresh as LoadState.Error).error + ) { onRefresh() } + } + + is LoadState.Loading -> { + binding.progressBar.show() + } + } + } + } + + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + val firstPos = (binding.recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + if (firstPos == 0 && positionStart == 0 && adapter.itemCount != itemCount) { + binding.recyclerView.post { + if (getView() != null) { + if (isSwipeToRefreshEnabled) { + binding.recyclerView.scrollBy( + 0, + Utils.dpToPx(requireContext(), -30) + ) + } else { + binding.recyclerView.scrollToPosition(0) + } + } + } + } + if (readingOrder == ReadingOrder.OLDEST_FIRST) { + updateReadingPositionForOldestFirst(adapter) + } + } + }) + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.statuses.collectLatest { pagingData -> + adapter.submitData(pagingData) + } + } + + viewLifecycleOwner.lifecycleScope.launch { + eventHub.events.collect { event -> + when (event) { + is PreferenceChangedEvent -> { + onPreferenceChanged(adapter, event.preferenceKey) + } + + is StatusComposedEvent -> { + val status = event.status + handleStatusComposeEvent(adapter, status) + } + } + } + } + + updateRelativeTimePeriodically(preferences) { + adapter.notifyItemRangeChanged( + 0, + adapter.itemCount, + listOf(StatusBaseViewHolder.Key.KEY_CREATED) + ) + } + } + + override fun onDestroyView() { + // Clear the adapter to prevent leaking the View + adapter = null + super.onDestroyView() + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + if (isSwipeToRefreshEnabled) { + menuInflater.inflate(R.menu.fragment_timeline, menu) + menu.findItem(R.id.action_refresh)?.apply { + icon = IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_refresh).apply { + sizeDp = 20 + colorInt = + MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary) + } + } + } + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_refresh -> { + if (isSwipeToRefreshEnabled) { + binding.swipeRefreshLayout.isRefreshing = true + + refreshContent() + true + } else { + false + } + } + + else -> false + } + } + + /** + * Set the correct reading position in the timeline after the user clicked "Load more", + * assuming the reading position should be below the freshly-loaded statuses. + */ + // Note: The positionStart parameter to onItemRangeInserted() does not always + // match the adapter position where data was inserted (which is why loadMorePosition + // is tracked manually, see this bug report for another example: + // https://github.com/android/architecture-components-samples/issues/726). + private fun updateReadingPositionForOldestFirst(adapter: TimelinePagingAdapter) { + var position = loadMorePosition ?: return + val statusIdBelowLoadMore = statusIdBelowLoadMore ?: return + + var status: StatusViewData? + while (adapter.peek(position).let { + status = it + it != null + } + ) { + if (status?.id == statusIdBelowLoadMore) { + val lastVisiblePosition = + (binding.recyclerView.layoutManager as LinearLayoutManager).findLastVisibleItemPosition() + if (position > lastVisiblePosition) { + binding.recyclerView.scrollToPosition(position) + } + break + } + position++ + } + loadMorePosition = null + } + + private fun setupSwipeRefreshLayout() { + binding.swipeRefreshLayout.isEnabled = isSwipeToRefreshEnabled + binding.swipeRefreshLayout.setOnRefreshListener(this) + } + + private fun setupRecyclerView(adapter: TimelinePagingAdapter) { + binding.recyclerView.setAccessibilityDelegateCompat( + ListStatusAccessibilityDelegate(binding.recyclerView, this) { pos -> + if (pos in 0 until adapter.itemCount) { + adapter.peek(pos) + } else { + null + } + } + ) + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(context) + val divider = DividerItemDecoration(context, RecyclerView.VERTICAL) + binding.recyclerView.addItemDecoration(divider) + + val recyclerViewBottomPadding = if ((activity as? ActionButtonActivity?)?.actionButton != null) { + resources.getDimensionPixelSize(R.dimen.recyclerview_bottom_padding_actionbutton) + } else { + resources.getDimensionPixelSize(R.dimen.recyclerview_bottom_padding_no_actionbutton) + } + + binding.recyclerView.updatePadding(bottom = recyclerViewBottomPadding) + + // CWs are expanded without animation, buttons animate itself, we don't need it basically + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + binding.recyclerView.adapter = adapter + } + + override fun onRefresh() { + binding.statusView.hide() + + adapter?.refresh() + } + + override val onMoreTranslate = + { translate: Boolean, position: Int -> + if (translate) { + onTranslate(position) + } else { + onUntranslate( + position + ) + } + } + + override fun onReply(position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + super.reply(status.status) + } + + override fun onReblog(reblog: Boolean, position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.reblog(reblog, status) + } + + private fun onTranslate(position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewLifecycleOwner.lifecycleScope.launch { + viewModel.translate(status) + .onFailure { + Snackbar.make( + requireView(), + getString(R.string.ui_error_translate, it.message), + Snackbar.LENGTH_LONG + ).show() + } + } + } + + override fun onUntranslate(position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.untranslate(status) + } + + override fun onFavourite(favourite: Boolean, position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.favorite(favourite, status) + } + + override fun onBookmark(bookmark: Boolean, position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.bookmark(bookmark, status) + } + + override fun onVoteInPoll(position: Int, choices: List<Int>) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.voteInPoll(choices, status) + } + + override fun clearWarningAction(position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.clearWarning(status) + } + + override fun onMore(view: View, position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + super.more( + status.status, + view, + position, + (status.translation as? TranslationViewData.Loaded)?.data + ) + } + + override fun onOpenReblog(position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + super.openReblog(status.status) + } + + override fun onExpandedChange(expanded: Boolean, position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.changeExpanded(expanded, status) + } + + override fun onContentHiddenChange(isShowing: Boolean, position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.changeContentShowing(isShowing, status) + } + + override fun onShowReblogs(position: Int) { + val statusId = adapter?.peek(position)?.asStatusOrNull()?.id ?: return + val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, statusId) + activity?.startActivityWithSlideInAnimation(intent) + } + + override fun onShowFavs(position: Int) { + val statusId = adapter?.peek(position)?.asStatusOrNull()?.id ?: return + val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, statusId) + activity?.startActivityWithSlideInAnimation(intent) + } + + override fun onLoadMore(position: Int) { + val adapter = this.adapter + val placeholder = adapter?.peek(position)?.asPlaceholderOrNull() ?: return + loadMorePosition = position + statusIdBelowLoadMore = + if (position + 1 < adapter.itemCount) adapter.peek(position + 1)?.id else null + viewModel.loadMore(placeholder.id) + } + + override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.changeContentCollapsed(isCollapsed, status) + } + + override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + super.viewMedia( + attachmentIndex, + AttachmentViewData.list(status), + view + ) + } + + override fun onViewThread(position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + super.viewThread(status.actionable.id, status.actionable.url) + } + + override fun onViewTag(tag: String) { + if (viewModel.kind == TimelineViewModel.Kind.TAG && viewModel.tags.size == 1 && + viewModel.tags.contains(tag) + ) { + // If already viewing a tag page, then ignore any request to view that tag again. + return + } + super.viewTag(tag) + } + + override fun onViewAccount(id: String) { + if (( + viewModel.kind == TimelineViewModel.Kind.USER || + viewModel.kind == TimelineViewModel.Kind.USER_WITH_REPLIES + ) && + viewModel.id == id + ) { + /* If already viewing an account page, then any requests to view that account page + * should be ignored. */ + return + } + super.viewAccount(id) + } + + private fun onPreferenceChanged(adapter: TimelinePagingAdapter, key: String) { + when (key) { + PrefKeys.MEDIA_PREVIEW_ENABLED -> { + val enabled = accountManager.activeAccount!!.mediaPreviewEnabled + val oldMediaPreviewEnabled = adapter.mediaPreviewEnabled + if (enabled != oldMediaPreviewEnabled) { + adapter.mediaPreviewEnabled = enabled + adapter.notifyItemRangeChanged(0, adapter.itemCount) + } + } + + PrefKeys.READING_ORDER -> { + readingOrder = ReadingOrder.from( + preferences.getString(PrefKeys.READING_ORDER, null) + ) + } + } + } + + private fun handleStatusComposeEvent(adapter: TimelinePagingAdapter, status: Status) { + when (kind) { + TimelineViewModel.Kind.HOME, + TimelineViewModel.Kind.PUBLIC_FEDERATED, + TimelineViewModel.Kind.PUBLIC_LOCAL, + TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES -> adapter.refresh() + + TimelineViewModel.Kind.USER, + TimelineViewModel.Kind.USER_WITH_REPLIES -> if (status.account.id == viewModel.id) { + adapter.refresh() + } + + TimelineViewModel.Kind.TAG, + TimelineViewModel.Kind.FAVOURITES, + TimelineViewModel.Kind.LIST, + TimelineViewModel.Kind.BOOKMARKS, + TimelineViewModel.Kind.USER_PINNED -> return + } + } + + public override fun removeItem(position: Int) { + val status = adapter?.peek(position)?.asStatusOrNull() ?: return + viewModel.removeStatusWithId(status.id) + } + + private fun actionButtonPresent(): Boolean { + return viewModel.kind != TimelineViewModel.Kind.TAG && + viewModel.kind != TimelineViewModel.Kind.FAVOURITES && + viewModel.kind != TimelineViewModel.Kind.BOOKMARKS && + activity is ActionButtonActivity + } + + private var talkBackWasEnabled = false + + override fun onPause() { + super.onPause() + (binding.recyclerView.layoutManager as? LinearLayoutManager)?.findFirstVisibleItemPosition() + ?.let { position -> + if (position != RecyclerView.NO_POSITION) { + adapter?.snapshot()?.getOrNull(position)?.id?.let { statusId -> + viewModel.saveReadingPosition(statusId) + } + } + } + } + + override fun onResume() { + super.onResume() + val a11yManager = requireContext().getSystemService<AccessibilityManager>() + + val wasEnabled = talkBackWasEnabled + talkBackWasEnabled = a11yManager?.isEnabled == true + Log.d(TAG, "talkback was enabled: $wasEnabled, now $talkBackWasEnabled") + if (talkBackWasEnabled && !wasEnabled) { + val adapter = requireNotNull(this.adapter) + adapter.notifyItemRangeChanged(0, adapter.itemCount) + } + } + + override fun onReselect() { + if (view != null) { + binding.recyclerView.layoutManager?.scrollToPosition(0) + binding.recyclerView.stopScroll() + } + } + + override fun refreshContent() { + onRefresh() + } + + companion object { + private const val TAG = "TimelineF" // logging tag + private const val KIND_ARG = "kind" + private const val ID_ARG = "id" + private const val HASHTAGS_ARG = "hashtags" + private const val ARG_ENABLE_SWIPE_TO_REFRESH = "enableSwipeToRefresh" + + fun newInstance( + kind: TimelineViewModel.Kind, + hashtagOrId: String? = null, + enableSwipeToRefresh: Boolean = true + ): TimelineFragment { + val fragment = TimelineFragment() + val arguments = Bundle(3) + arguments.putString(KIND_ARG, kind.name) + arguments.putString(ID_ARG, hashtagOrId) + arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, enableSwipeToRefresh) + fragment.arguments = arguments + return fragment + } + + @JvmStatic + fun newHashtagInstance(hashtags: List<String>): TimelineFragment { + val fragment = TimelineFragment() + val arguments = Bundle(3) + arguments.putString(KIND_ARG, TimelineViewModel.Kind.TAG.name) + arguments.putStringArrayList(HASHTAGS_ARG, ArrayList(hashtags)) + arguments.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH, true) + fragment.arguments = arguments + return fragment + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt new file mode 100644 index 0000000..bc74d86 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelinePagingAdapter.kt @@ -0,0 +1,142 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.timeline + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.paging.PagingDataAdapter +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.PlaceholderViewHolder +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.adapter.StatusViewHolder +import com.keylesspalace.tusky.databinding.ItemStatusPlaceholderBinding +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.StatusViewData + +class TimelinePagingAdapter( + private var statusDisplayOptions: StatusDisplayOptions, + private val statusListener: StatusActionListener +) : PagingDataAdapter<StatusViewData, RecyclerView.ViewHolder>(TimelineDifferCallback) { + + var mediaPreviewEnabled: Boolean + get() = statusDisplayOptions.mediaPreviewEnabled + set(mediaPreviewEnabled) { + statusDisplayOptions = statusDisplayOptions.copy( + mediaPreviewEnabled = mediaPreviewEnabled + ) + } + + init { + stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY + } + + override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(viewGroup.context) + return when (viewType) { + VIEW_TYPE_STATUS_FILTERED -> { + StatusViewHolder(inflater.inflate(R.layout.item_status_wrapper, viewGroup, false)) + } + VIEW_TYPE_PLACEHOLDER -> { + PlaceholderViewHolder( + ItemStatusPlaceholderBinding.inflate(inflater, viewGroup, false), + statusListener + ) + } + else -> { + StatusViewHolder(inflater.inflate(R.layout.item_status, viewGroup, false)) + } + } + } + + override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) { + bindViewHolder(viewHolder, position, null) + } + + override fun onBindViewHolder( + viewHolder: RecyclerView.ViewHolder, + position: Int, + payloads: List<*> + ) { + bindViewHolder(viewHolder, position, payloads) + } + + private fun bindViewHolder( + viewHolder: RecyclerView.ViewHolder, + position: Int, + payloads: List<*>? + ) { + val status = getItem(position) + if (status is StatusViewData.Placeholder) { + val holder = viewHolder as PlaceholderViewHolder + holder.setup(status.isLoading) + } else if (status is StatusViewData.Concrete) { + val holder = viewHolder as StatusViewHolder + holder.setupWithStatus( + status, + statusListener, + statusDisplayOptions, + if (payloads != null && payloads.isNotEmpty()) payloads[0] else null + ) + } + } + + override fun getItemViewType(position: Int): Int { + val viewData = getItem(position) + return if (viewData is StatusViewData.Placeholder) { + VIEW_TYPE_PLACEHOLDER + } else if (viewData?.filterAction == Filter.Action.WARN) { + VIEW_TYPE_STATUS_FILTERED + } else { + VIEW_TYPE_STATUS + } + } + + companion object { + private const val VIEW_TYPE_STATUS = 0 + private const val VIEW_TYPE_STATUS_FILTERED = 1 + private const val VIEW_TYPE_PLACEHOLDER = 2 + + val TimelineDifferCallback = object : DiffUtil.ItemCallback<StatusViewData>() { + override fun areItemsTheSame( + oldItem: StatusViewData, + newItem: StatusViewData + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: StatusViewData, + newItem: StatusViewData + ): Boolean { + return false // Items are different always. It allows to refresh timestamp on every view holder update + } + + override fun getChangePayload(oldItem: StatusViewData, newItem: StatusViewData): Any? { + return if (oldItem == newItem) { + // If items are equal - update timestamp only + listOf(StatusBaseViewHolder.Key.KEY_CREATED) + } else { + // If items are different - update the whole view holder + null + } + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt new file mode 100644 index 0000000..b3f0de1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/TimelineTypeMappers.kt @@ -0,0 +1,197 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.timeline + +import com.keylesspalace.tusky.db.entity.HomeTimelineData +import com.keylesspalace.tusky.db.entity.HomeTimelineEntity +import com.keylesspalace.tusky.db.entity.TimelineAccountEntity +import com.keylesspalace.tusky.db.entity.TimelineStatusEntity +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.keylesspalace.tusky.viewdata.TranslationViewData +import java.util.Date + +data class Placeholder( + val id: String, + val loading: Boolean +) + +fun TimelineAccount.toEntity(tuskyAccountId: Long): TimelineAccountEntity { + return TimelineAccountEntity( + serverId = id, + tuskyAccountId = tuskyAccountId, + localUsername = localUsername, + username = username, + displayName = name, + url = url, + avatar = avatar, + emojis = emojis, + bot = bot + ) +} + +fun TimelineAccountEntity.toAccount(): TimelineAccount { + return TimelineAccount( + id = serverId, + localUsername = localUsername, + username = username, + displayName = displayName, + note = "", + url = url, + avatar = avatar, + bot = bot, + emojis = emojis + ) +} + +fun Placeholder.toEntity(tuskyAccountId: Long): HomeTimelineEntity { + return HomeTimelineEntity( + id = this.id, + tuskyAccountId = tuskyAccountId, + statusId = null, + reblogAccountId = null, + loading = this.loading + ) +} + +fun Status.toEntity( + tuskyAccountId: Long, + expanded: Boolean, + contentShowing: Boolean, + contentCollapsed: Boolean +) = TimelineStatusEntity( + serverId = id, + url = actionableStatus.url, + tuskyAccountId = tuskyAccountId, + authorServerId = actionableStatus.account.id, + inReplyToId = actionableStatus.inReplyToId, + inReplyToAccountId = actionableStatus.inReplyToAccountId, + content = actionableStatus.content, + createdAt = actionableStatus.createdAt.time, + editedAt = actionableStatus.editedAt?.time, + emojis = actionableStatus.emojis, + reblogsCount = actionableStatus.reblogsCount, + favouritesCount = actionableStatus.favouritesCount, + reblogged = actionableStatus.reblogged, + favourited = actionableStatus.favourited, + bookmarked = actionableStatus.bookmarked, + sensitive = actionableStatus.sensitive, + spoilerText = actionableStatus.spoilerText, + visibility = actionableStatus.visibility, + attachments = actionableStatus.attachments, + mentions = actionableStatus.mentions, + tags = actionableStatus.tags, + application = actionableStatus.application, + poll = actionableStatus.poll, + muted = actionableStatus.muted, + expanded = expanded, + contentShowing = contentShowing, + contentCollapsed = contentCollapsed, + pinned = actionableStatus.pinned, + card = actionableStatus.card, + repliesCount = actionableStatus.repliesCount, + language = actionableStatus.language, + filtered = actionableStatus.filtered.orEmpty() +) + +fun TimelineStatusEntity.toStatus( + account: TimelineAccountEntity +) = Status( + id = serverId, + url = url, + account = account.toAccount(), + inReplyToId = inReplyToId, + inReplyToAccountId = inReplyToAccountId, + reblog = null, + content = content, + createdAt = Date(createdAt), + editedAt = editedAt?.let { Date(it) }, + emojis = emojis, + reblogsCount = reblogsCount, + favouritesCount = favouritesCount, + reblogged = reblogged, + favourited = favourited, + bookmarked = bookmarked, + sensitive = sensitive, + spoilerText = spoilerText, + visibility = visibility, + attachments = attachments, + mentions = mentions, + tags = tags, + application = application, + pinned = false, + muted = muted, + poll = poll, + card = card, + repliesCount = repliesCount, + language = language, + filtered = filtered, +) + +fun HomeTimelineData.toViewData(isDetailed: Boolean = false, translation: TranslationViewData? = null): StatusViewData { + if (this.account == null || this.status == null) { + return StatusViewData.Placeholder(this.id, loading) + } + + val originalStatus = status.toStatus(account) + val status = if (reblogAccount != null) { + Status( + id = id, + // no url for reblogs + url = null, + account = reblogAccount.toAccount(), + inReplyToId = status.inReplyToId, + inReplyToAccountId = status.inReplyToAccountId, + reblog = originalStatus, + content = status.content, + // lie but whatever? + createdAt = Date(status.createdAt), + editedAt = null, + emojis = emptyList(), + reblogsCount = status.reblogsCount, + favouritesCount = status.favouritesCount, + reblogged = status.reblogged, + favourited = status.favourited, + bookmarked = status.bookmarked, + sensitive = status.sensitive, + spoilerText = status.spoilerText, + visibility = status.visibility, + attachments = emptyList(), + mentions = emptyList(), + tags = emptyList(), + application = null, + pinned = false, + muted = status.muted, + poll = null, + card = null, + repliesCount = status.repliesCount, + language = status.language, + filtered = status.filtered, + ) + } else { + originalStatus + } + + return StatusViewData.Concrete( + status = status, + isExpanded = this.status.expanded, + isShowingContent = this.status.contentShowing, + isCollapsed = this.status.contentCollapsed, + isDetailed = isDetailed, + translation = translation, + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/util/TimelineUtils.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/util/TimelineUtils.kt new file mode 100644 index 0000000..91d436f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/util/TimelineUtils.kt @@ -0,0 +1,16 @@ +package com.keylesspalace.tusky.components.timeline.util + +import com.squareup.moshi.JsonDataException +import java.io.IOException +import retrofit2.HttpException + +fun Throwable.isExpected() = + this is IOException || this is HttpException || this is JsonDataException + +inline fun <T> ifExpected(t: Throwable, cb: () -> T): T { + if (t.isExpected()) { + return cb() + } else { + throw t + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt new file mode 100644 index 0000000..73bc5e7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineRemoteMediator.kt @@ -0,0 +1,193 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.timeline.viewmodel + +import android.util.Log +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import androidx.room.withTransaction +import com.keylesspalace.tusky.components.timeline.Placeholder +import com.keylesspalace.tusky.components.timeline.toEntity +import com.keylesspalace.tusky.components.timeline.util.ifExpected +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.entity.HomeTimelineData +import com.keylesspalace.tusky.db.entity.HomeTimelineEntity +import com.keylesspalace.tusky.db.entity.TimelineStatusEntity +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import retrofit2.HttpException + +@OptIn(ExperimentalPagingApi::class) +class CachedTimelineRemoteMediator( + accountManager: AccountManager, + private val api: MastodonApi, + private val db: AppDatabase, +) : RemoteMediator<Int, HomeTimelineData>() { + + private var initialRefresh = false + + private val timelineDao = db.timelineDao() + private val statusDao = db.timelineStatusDao() + private val accountDao = db.timelineAccountDao() + private val activeAccount = accountManager.activeAccount!! + + override suspend fun load( + loadType: LoadType, + state: PagingState<Int, HomeTimelineData> + ): MediatorResult { + if (!activeAccount.isLoggedIn()) { + return MediatorResult.Success(endOfPaginationReached = true) + } + + try { + var dbEmpty = false + + val topPlaceholderId = if (loadType == LoadType.REFRESH) { + timelineDao.getTopPlaceholderId(activeAccount.id) + } else { + null // don't execute the query if it is not needed + } + + if (!initialRefresh && loadType == LoadType.REFRESH) { + val topId = timelineDao.getTopId(activeAccount.id) + topId?.let { cachedTopId -> + val statusResponse = api.homeTimeline( + maxId = cachedTopId, + // so already existing placeholders don't get accidentally overwritten + sinceId = topPlaceholderId, + limit = state.config.pageSize + ) + + val statuses = statusResponse.body() + if (statusResponse.isSuccessful && statuses != null) { + db.withTransaction { + replaceStatusRange(statuses, state) + } + } + } + initialRefresh = true + dbEmpty = topId == null + } + + val statusResponse = when (loadType) { + LoadType.REFRESH -> { + api.homeTimeline(sinceId = topPlaceholderId, limit = state.config.pageSize) + } + LoadType.PREPEND -> { + return MediatorResult.Success(endOfPaginationReached = true) + } + LoadType.APPEND -> { + val maxId = state.pages.findLast { it.data.isNotEmpty() }?.data?.lastOrNull()?.status?.serverId + api.homeTimeline(maxId = maxId, limit = state.config.pageSize) + } + } + + val statuses = statusResponse.body() + if (!statusResponse.isSuccessful || statuses == null) { + return MediatorResult.Error(HttpException(statusResponse)) + } + + db.withTransaction { + val overlappedStatuses = replaceStatusRange(statuses, state) + + /* In case we loaded a whole page and there was no overlap with existing statuses, + we insert a placeholder because there might be even more unknown statuses */ + if (loadType == LoadType.REFRESH && overlappedStatuses == 0 && statuses.size == state.config.pageSize && !dbEmpty) { + /* This overrides the last of the newly loaded statuses with a placeholder + to guarantee the placeholder has an id that exists on the server as not all + servers handle client generated ids as expected */ + timelineDao.insertHomeTimelineItem( + Placeholder(statuses.last().id, loading = false).toEntity(activeAccount.id) + ) + } + } + return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty()) + } catch (e: Exception) { + return ifExpected(e) { + Log.w(TAG, "Failed to load timeline", e) + MediatorResult.Error(e) + } + } + } + + /** + * Deletes all statuses in a given range and inserts new statuses. + * This is necessary so statuses that have been deleted on the server are cleaned up. + * Should be run in a transaction as it executes multiple db updates + * @param statuses the new statuses + * @return the number of old statuses that have been cleared from the database + */ + private suspend fun replaceStatusRange( + statuses: List<Status>, + state: PagingState<Int, HomeTimelineData> + ): Int { + val overlappedStatuses = if (statuses.isNotEmpty()) { + timelineDao.deleteRange(activeAccount.id, statuses.last().id, statuses.first().id) + } else { + 0 + } + + for (status in statuses) { + accountDao.insert(status.account.toEntity(activeAccount.id)) + status.reblog?.account?.toEntity(activeAccount.id)?.let { rebloggedAccount -> + accountDao.insert(rebloggedAccount) + } + + // check if we already have one of the newly loaded statuses cached locally + // in case we do, copy the local state (expanded, contentShowing, contentCollapsed) over so it doesn't get lost + var oldStatus: TimelineStatusEntity? = null + for (page in state.pages) { + oldStatus = page.data.find { s -> + s.status?.serverId == status.actionableId + }?.status + if (oldStatus != null) break + } + + val expanded = oldStatus?.expanded ?: activeAccount.alwaysOpenSpoiler + val contentShowing = oldStatus?.contentShowing ?: (activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive) + val contentCollapsed = oldStatus?.contentCollapsed ?: true + + statusDao.insert( + status.actionableStatus.toEntity( + tuskyAccountId = activeAccount.id, + expanded = expanded, + contentShowing = contentShowing, + contentCollapsed = contentCollapsed + ) + ) + timelineDao.insertHomeTimelineItem( + HomeTimelineEntity( + tuskyAccountId = activeAccount.id, + id = status.id, + statusId = status.actionableId, + reblogAccountId = if (status.reblog != null) { + status.account.id + } else { + null + } + ) + ) + } + return overlappedStatuses + } + + companion object { + private const val TAG = "CachedTimelineRM" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt new file mode 100644 index 0000000..a1e2bbf --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/CachedTimelineViewModel.kt @@ -0,0 +1,313 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.timeline.viewmodel + +import android.content.SharedPreferences +import android.util.Log +import androidx.lifecycle.viewModelScope +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingSource +import androidx.paging.cachedIn +import androidx.paging.filter +import androidx.paging.map +import androidx.room.withTransaction +import at.connyduck.calladapter.networkresult.NetworkResult +import at.connyduck.calladapter.networkresult.map +import at.connyduck.calladapter.networkresult.onFailure +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder.NEWEST_FIRST +import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder.OLDEST_FIRST +import com.keylesspalace.tusky.components.timeline.Placeholder +import com.keylesspalace.tusky.components.timeline.toEntity +import com.keylesspalace.tusky.components.timeline.toViewData +import com.keylesspalace.tusky.components.timeline.util.ifExpected +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.entity.HomeTimelineData +import com.keylesspalace.tusky.db.entity.HomeTimelineEntity +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.network.FilterModel +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.usecase.TimelineCases +import com.keylesspalace.tusky.util.EmptyPagingSource +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.keylesspalace.tusky.viewdata.TranslationViewData +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.launch +import retrofit2.HttpException + +/** + * TimelineViewModel that caches all statuses in a local database + */ +@HiltViewModel +class CachedTimelineViewModel @Inject constructor( + timelineCases: TimelineCases, + private val api: MastodonApi, + eventHub: EventHub, + accountManager: AccountManager, + sharedPreferences: SharedPreferences, + filterModel: FilterModel, + private val db: AppDatabase +) : TimelineViewModel( + timelineCases, + api, + eventHub, + accountManager, + sharedPreferences, + filterModel +) { + + private var currentPagingSource: PagingSource<Int, HomeTimelineData>? = null + + /** Map from status id to translation. */ + private val translations = MutableStateFlow(mapOf<String, TranslationViewData>()) + + @OptIn(ExperimentalPagingApi::class) + override val statuses = Pager( + config = PagingConfig( + pageSize = LOAD_AT_ONCE + ), + remoteMediator = CachedTimelineRemoteMediator(accountManager, api, db), + pagingSourceFactory = { + val activeAccount = accountManager.activeAccount + if (activeAccount == null) { + EmptyPagingSource() + } else { + db.timelineDao().getHomeTimeline(activeAccount.id) + }.also { newPagingSource -> + this.currentPagingSource = newPagingSource + } + } + ).flow + // Apply cachedIn() early to be able to combine with translation flow. + // This will not cache ViewData's but practically we don't need this. + // If you notice that this flow is used in more than once place consider + // adding another cachedIn() for the overall result. + .cachedIn(viewModelScope) + .combine(translations) { pagingData, translations -> + pagingData.map { timelineData -> + val translation = translations[timelineData.status?.serverId] + timelineData.toViewData( + isDetailed = false, + translation = translation + ) + }.filter { statusViewData -> + shouldFilterStatus(statusViewData) != Filter.Action.HIDE + } + } + .flowOn(Dispatchers.Default) + + override fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) { + // handled by CacheUpdater + } + + override fun changeExpanded(expanded: Boolean, status: StatusViewData.Concrete) { + viewModelScope.launch { + db.timelineStatusDao() + .setExpanded(accountManager.activeAccount!!.id, status.actionableId, expanded) + } + } + + override fun changeContentShowing(isShowing: Boolean, status: StatusViewData.Concrete) { + viewModelScope.launch { + db.timelineStatusDao() + .setContentShowing(accountManager.activeAccount!!.id, status.actionableId, isShowing) + } + } + + override fun changeContentCollapsed(isCollapsed: Boolean, status: StatusViewData.Concrete) { + viewModelScope.launch { + db.timelineStatusDao() + .setContentCollapsed(accountManager.activeAccount!!.id, status.actionableId, isCollapsed) + } + } + + override fun clearWarning(status: StatusViewData.Concrete) { + viewModelScope.launch { + db.timelineStatusDao().clearWarning(accountManager.activeAccount!!.id, status.actionableId) + } + } + + override fun removeStatusWithId(id: String) { + // handled by CacheUpdater + } + + override fun loadMore(placeholderId: String) { + viewModelScope.launch { + try { + val timelineDao = db.timelineDao() + val statusDao = db.timelineStatusDao() + val accountDao = db.timelineAccountDao() + + val activeAccount = accountManager.activeAccount!! + + timelineDao.insertHomeTimelineItem( + Placeholder(placeholderId, loading = true).toEntity( + activeAccount.id + ) + ) + + val response = db.withTransaction { + val idAbovePlaceholder = timelineDao.getIdAbove(activeAccount.id, placeholderId) + val idBelowPlaceholder = timelineDao.getIdBelow(activeAccount.id, placeholderId) + when (readingOrder) { + // Using minId, loads up to LOAD_AT_ONCE statuses with IDs immediately + // after minId and no larger than maxId + OLDEST_FIRST -> api.homeTimeline( + maxId = idAbovePlaceholder, + minId = idBelowPlaceholder, + limit = LOAD_AT_ONCE + ) + // Using sinceId, loads up to LOAD_AT_ONCE statuses immediately before + // maxId, and no smaller than minId. + NEWEST_FIRST -> api.homeTimeline( + maxId = idAbovePlaceholder, + sinceId = idBelowPlaceholder, + limit = LOAD_AT_ONCE + ) + } + } + + val statuses = response.body() + if (!response.isSuccessful || statuses == null) { + loadMoreFailed(placeholderId, HttpException(response)) + return@launch + } + + db.withTransaction { + timelineDao.deleteHomeTimelineItem(activeAccount.id, placeholderId) + + val overlappedStatuses = if (statuses.isNotEmpty()) { + timelineDao.deleteRange( + activeAccount.id, + statuses.last().id, + statuses.first().id + ) + } else { + 0 + } + + for (status in statuses) { + accountDao.insert(status.account.toEntity(activeAccount.id)) + status.reblog?.account?.toEntity(activeAccount.id) + ?.let { rebloggedAccount -> + accountDao.insert(rebloggedAccount) + } + statusDao.insert( + status.actionableStatus.toEntity( + tuskyAccountId = activeAccount.id, + expanded = activeAccount.alwaysOpenSpoiler, + contentShowing = activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive, + contentCollapsed = true + ) + ) + timelineDao.insertHomeTimelineItem( + HomeTimelineEntity( + tuskyAccountId = activeAccount.id, + id = status.id, + statusId = status.actionableId, + reblogAccountId = if (status.reblog != null) { + status.account.id + } else { + null + } + ) + ) + } + + /* In case we loaded a whole page and there was no overlap with existing statuses, + we insert a placeholder because there might be even more unknown statuses */ + if (overlappedStatuses == 0 && statuses.size == LOAD_AT_ONCE) { + /* This overrides the first/last of the newly loaded statuses with a placeholder + to guarantee the placeholder has an id that exists on the server as not all + servers handle client generated ids as expected */ + val idToConvert = when (readingOrder) { + OLDEST_FIRST -> statuses.first().id + NEWEST_FIRST -> statuses.last().id + } + timelineDao.insertHomeTimelineItem( + Placeholder( + idToConvert, + loading = false + ).toEntity(activeAccount.id) + ) + } + } + } catch (e: Exception) { + ifExpected(e) { + loadMoreFailed(placeholderId, e) + } + } + } + } + + private suspend fun loadMoreFailed(placeholderId: String, e: Exception) { + Log.w("CachedTimelineVM", "failed loading statuses", e) + val activeAccount = accountManager.activeAccount!! + db.timelineDao() + .insertHomeTimelineItem(Placeholder(placeholderId, loading = false).toEntity(activeAccount.id)) + } + + override fun fullReload() { + viewModelScope.launch { + val activeAccount = accountManager.activeAccount!! + db.timelineDao().removeAllHomeTimelineItems(activeAccount.id) + } + } + + override fun saveReadingPosition(statusId: String) { + accountManager.activeAccount?.let { account -> + Log.d(TAG, "Saving position at: $statusId") + account.lastVisibleHomeTimelineStatusId = statusId + accountManager.saveAccount(account) + } + } + + override suspend fun invalidate() { + // invalidating when we don't have statuses yet can cause empty timelines because it cancels the network load + if (db.timelineDao().getHomeTimelineItemCount(accountManager.activeAccount!!.id) > 0) { + currentPagingSource?.invalidate() + } + } + + override suspend fun translate(status: StatusViewData.Concrete): NetworkResult<Unit> { + translations.value = translations.value + (status.id to TranslationViewData.Loading) + return timelineCases.translate(status.actionableId) + .map { translation -> + translations.value = + translations.value + (status.id to TranslationViewData.Loaded(translation)) + } + .onFailure { + translations.value = translations.value - status.id + } + } + + override fun untranslate(status: StatusViewData.Concrete) { + translations.value = translations.value - status.id + } + + companion object { + private const val TAG = "CachedTimelineViewModel" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelinePagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelinePagingSource.kt new file mode 100644 index 0000000..d1c9ce2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelinePagingSource.kt @@ -0,0 +1,36 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.timeline.viewmodel + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.keylesspalace.tusky.viewdata.StatusViewData + +class NetworkTimelinePagingSource( + private val viewModel: NetworkTimelineViewModel +) : PagingSource<String, StatusViewData>() { + + override fun getRefreshKey(state: PagingState<String, StatusViewData>): String? = null + + override suspend fun load(params: LoadParams<String>): LoadResult<String, StatusViewData> { + return if (params is LoadParams.Refresh) { + val list = viewModel.statusData.toList() + LoadResult.Page(list, null, viewModel.nextKey) + } else { + LoadResult.Page(emptyList(), null, null) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt new file mode 100644 index 0000000..f19b224 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineRemoteMediator.kt @@ -0,0 +1,142 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.timeline.viewmodel + +import android.util.Log +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import com.keylesspalace.tusky.components.timeline.util.ifExpected +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.util.HttpHeaderLink +import com.keylesspalace.tusky.util.toViewData +import com.keylesspalace.tusky.viewdata.StatusViewData +import retrofit2.HttpException + +@OptIn(ExperimentalPagingApi::class) +class NetworkTimelineRemoteMediator( + private val accountManager: AccountManager, + private val viewModel: NetworkTimelineViewModel +) : RemoteMediator<String, StatusViewData>() { + + private val statusIds = mutableSetOf<String>() + + init { + if (viewModel.kind == TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES) { + statusIds.addAll(viewModel.statusData.map { it.id }) + } + } + + override suspend fun load( + loadType: LoadType, + state: PagingState<String, StatusViewData> + ): MediatorResult { + try { + val statusResponse = when (loadType) { + LoadType.REFRESH -> { + viewModel.fetchStatusesForKind(null, null, limit = state.config.pageSize) + } + LoadType.PREPEND -> { + return MediatorResult.Success(endOfPaginationReached = true) + } + LoadType.APPEND -> { + val maxId = viewModel.nextKey + if (maxId != null) { + viewModel.fetchStatusesForKind(maxId, null, limit = state.config.pageSize) + } else { + return MediatorResult.Success(endOfPaginationReached = true) + } + } + } + + val statuses = statusResponse.body() + if (!statusResponse.isSuccessful || statuses == null) { + return MediatorResult.Error(HttpException(statusResponse)) + } + + val activeAccount = accountManager.activeAccount!! + + val data = statuses.map { status -> + + val oldStatus = viewModel.statusData.find { s -> + s.asStatusOrNull()?.id == status.id + }?.asStatusOrNull() + + val contentShowing = oldStatus?.isShowingContent ?: (activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive) + val expanded = oldStatus?.isExpanded ?: activeAccount.alwaysOpenSpoiler + val contentCollapsed = oldStatus?.isCollapsed ?: true + + status.toViewData( + isShowingContent = contentShowing, + isExpanded = expanded, + isCollapsed = contentCollapsed + ) + } + + if (loadType == LoadType.REFRESH && viewModel.statusData.isNotEmpty()) { + val insertPlaceholder = if (statuses.isNotEmpty()) { + !viewModel.statusData.removeAll { statusViewData -> + statuses.any { status -> status.id == statusViewData.asStatusOrNull()?.id } + } + } else { + false + } + + if (viewModel.kind == TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES) { + statusIds.addAll(data.map { it.id }) + } + + viewModel.statusData.addAll(0, data) + + if (insertPlaceholder) { + viewModel.statusData[statuses.size - 1] = StatusViewData.Placeholder(statuses.last().id, false) + } + } else { + val linkHeader = statusResponse.headers()["Link"] + val links = HttpHeaderLink.parse(linkHeader) + val next = HttpHeaderLink.findByRelationType(links, "next") + + var filteredData = data + if (viewModel.kind == TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES) { + // Trending statuses use offset for paging, not IDs. If a new status has been added to the remote + // feed after we performed the initial fetch, then the feed will have moved, but our offset won't. + // As a result, we'd get repeat statuses. This addresses that. + filteredData = data.filter { !statusIds.contains(it.id) } + statusIds.addAll(filteredData.map { it.id }) + + viewModel.nextKey = next?.uri?.getQueryParameter("offset") + } else { + viewModel.nextKey = next?.uri?.getQueryParameter("max_id") + } + + viewModel.statusData.addAll(filteredData) + } + + viewModel.currentSource?.invalidate() + return MediatorResult.Success(endOfPaginationReached = statuses.isEmpty()) + } catch (e: Exception) { + return ifExpected(e) { + Log.w(TAG, "Failed to load timeline", e) + MediatorResult.Error(e) + } + } + } + + companion object { + private const val TAG = "NetworkTimelineRM" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt new file mode 100644 index 0000000..a40bb41 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/NetworkTimelineViewModel.kt @@ -0,0 +1,431 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.timeline.viewmodel + +import android.content.SharedPreferences +import android.util.Log +import androidx.lifecycle.viewModelScope +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.cachedIn +import androidx.paging.filter +import at.connyduck.calladapter.networkresult.NetworkResult +import at.connyduck.calladapter.networkresult.map +import at.connyduck.calladapter.networkresult.onFailure +import com.keylesspalace.tusky.appstore.BlockEvent +import com.keylesspalace.tusky.appstore.DomainMuteEvent +import com.keylesspalace.tusky.appstore.Event +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.MuteEvent +import com.keylesspalace.tusky.appstore.StatusChangedEvent +import com.keylesspalace.tusky.appstore.StatusDeletedEvent +import com.keylesspalace.tusky.appstore.UnfollowEvent +import com.keylesspalace.tusky.components.timeline.util.ifExpected +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.FilterModel +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.usecase.TimelineCases +import com.keylesspalace.tusky.util.getDomain +import com.keylesspalace.tusky.util.isLessThan +import com.keylesspalace.tusky.util.isLessThanOrEqual +import com.keylesspalace.tusky.util.toViewData +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.keylesspalace.tusky.viewdata.TranslationViewData +import dagger.hilt.android.lifecycle.HiltViewModel +import java.io.IOException +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import retrofit2.HttpException +import retrofit2.Response + +/** + * TimelineViewModel that caches all statuses in an in-memory list + */ +@HiltViewModel +class NetworkTimelineViewModel @Inject constructor( + timelineCases: TimelineCases, + private val api: MastodonApi, + eventHub: EventHub, + accountManager: AccountManager, + sharedPreferences: SharedPreferences, + filterModel: FilterModel +) : TimelineViewModel( + timelineCases, + api, + eventHub, + accountManager, + sharedPreferences, + filterModel +) { + + var currentSource: NetworkTimelinePagingSource? = null + + val statusData: MutableList<StatusViewData> = mutableListOf() + + var nextKey: String? = null + + @OptIn(ExperimentalPagingApi::class) + override val statuses = Pager( + config = PagingConfig( + pageSize = LOAD_AT_ONCE + ), + pagingSourceFactory = { + NetworkTimelinePagingSource( + viewModel = this + ).also { source -> + currentSource = source + } + }, + remoteMediator = NetworkTimelineRemoteMediator(accountManager, this) + ).flow + .map { pagingData -> + pagingData.filter(Dispatchers.Default.asExecutor()) { statusViewData -> + shouldFilterStatus(statusViewData) != Filter.Action.HIDE + } + } + .flowOn(Dispatchers.Default) + .cachedIn(viewModelScope) + + init { + viewModelScope.launch { + eventHub.events + .collect { event -> handleEvent(event) } + } + } + + private fun handleEvent(event: Event) { + when (event) { + is StatusChangedEvent -> handleStatusChangedEvent(event.status) + is UnfollowEvent -> { + if (kind == Kind.HOME) { + val id = event.accountId + removeAllByAccountId(id) + } + } + is BlockEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + val id = event.accountId + removeAllByAccountId(id) + } + } + is MuteEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + val id = event.accountId + removeAllByAccountId(id) + } + } + is DomainMuteEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + val instance = event.instance + removeAllByInstance(instance) + } + } + is StatusDeletedEvent -> { + if (kind != Kind.USER && kind != Kind.USER_WITH_REPLIES && kind != Kind.USER_PINNED) { + removeStatusWithId(event.statusId) + } + } + } + } + + override fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) { + status.copy( + status = status.status.copy(poll = newPoll) + ).update() + } + + override fun changeExpanded(expanded: Boolean, status: StatusViewData.Concrete) { + status.copy( + isExpanded = expanded + ).update() + } + + override fun changeContentShowing(isShowing: Boolean, status: StatusViewData.Concrete) { + status.copy( + isShowingContent = isShowing + ).update() + } + + override fun changeContentCollapsed(isCollapsed: Boolean, status: StatusViewData.Concrete) { + status.copy( + isCollapsed = isCollapsed + ).update() + } + + private fun removeAllByAccountId(accountId: String) { + statusData.removeAll { vd -> + val status = vd.asStatusOrNull()?.status ?: return@removeAll false + status.account.id == accountId || status.actionableStatus.account.id == accountId + } + currentSource?.invalidate() + } + + private fun removeAllByInstance(instance: String) { + statusData.removeAll { vd -> + val status = vd.asStatusOrNull()?.status ?: return@removeAll false + getDomain(status.account.url) == instance + } + currentSource?.invalidate() + } + + override fun removeStatusWithId(id: String) { + statusData.removeAll { vd -> + val status = vd.asStatusOrNull()?.status ?: return@removeAll false + status.id == id || status.reblog?.id == id + } + currentSource?.invalidate() + } + + override fun loadMore(placeholderId: String) { + viewModelScope.launch { + try { + val placeholderIndex = + statusData.indexOfFirst { it is StatusViewData.Placeholder && it.id == placeholderId } + statusData[placeholderIndex] = + StatusViewData.Placeholder(placeholderId, isLoading = true) + + val idAbovePlaceholder = statusData.getOrNull(placeholderIndex - 1)?.id + + val statusResponse = fetchStatusesForKind( + fromId = idAbovePlaceholder, + uptoId = null, + limit = 20 + ) + + val statuses = statusResponse.body() + if (!statusResponse.isSuccessful || statuses == null) { + loadMoreFailed(placeholderId, HttpException(statusResponse)) + return@launch + } + + statusData.removeAt(placeholderIndex) + + val activeAccount = accountManager.activeAccount!! + val data: MutableList<StatusViewData> = statuses.map { status -> + status.toViewData( + isShowingContent = activeAccount.alwaysShowSensitiveMedia || !status.actionableStatus.sensitive, + isExpanded = activeAccount.alwaysOpenSpoiler, + isCollapsed = true + ) + }.toMutableList() + + if (statuses.isNotEmpty()) { + val firstId = statuses.first().id + val lastId = statuses.last().id + val overlappedFrom = statusData.indexOfFirst { + it.asStatusOrNull()?.id?.isLessThanOrEqual(firstId) ?: false + } + val overlappedTo = statusData.indexOfFirst { + it.asStatusOrNull()?.id?.isLessThan(lastId) ?: false + } + + if (overlappedFrom < overlappedTo) { + data.mapIndexed { i, status -> + i to statusData.firstOrNull { + it.asStatusOrNull()?.id == status.id + }?.asStatusOrNull() + } + .filter { (_, oldStatus) -> oldStatus != null } + .forEach { (i, oldStatus) -> + data[i] = data[i].asStatusOrNull()!! + .copy( + isShowingContent = oldStatus!!.isShowingContent, + isExpanded = oldStatus.isExpanded, + isCollapsed = oldStatus.isCollapsed + ) + } + + statusData.removeAll { status -> + when (status) { + is StatusViewData.Placeholder -> lastId.isLessThan(status.id) && status.id.isLessThanOrEqual( + firstId + ) + + is StatusViewData.Concrete -> lastId.isLessThan(status.id) && status.id.isLessThanOrEqual( + firstId + ) + } + } + } else { + data[data.size - 1] = + StatusViewData.Placeholder(statuses.last().id, isLoading = false) + } + } + + statusData.addAll(placeholderIndex, data) + + currentSource?.invalidate() + } catch (e: Exception) { + ifExpected(e) { + loadMoreFailed(placeholderId, e) + } + } + } + } + + private fun loadMoreFailed(placeholderId: String, e: Exception) { + Log.w("NetworkTimelineVM", "failed loading statuses", e) + + val index = + statusData.indexOfFirst { it is StatusViewData.Placeholder && it.id == placeholderId } + statusData[index] = StatusViewData.Placeholder(placeholderId, isLoading = false) + + currentSource?.invalidate() + } + + private fun handleStatusChangedEvent(status: Status) { + updateStatusById(status.id) { oldViewData -> + status.toViewData( + isShowingContent = oldViewData.isShowingContent, + isExpanded = oldViewData.isExpanded, + isCollapsed = oldViewData.isCollapsed + ) + } + } + + override fun fullReload() { + nextKey = statusData.firstOrNull { it is StatusViewData.Concrete }?.asStatusOrNull()?.id + statusData.clear() + currentSource?.invalidate() + } + + override fun clearWarning(status: StatusViewData.Concrete) { + updateActionableStatusById(status.id) { + it.copy(filtered = emptyList()) + } + } + + override fun saveReadingPosition(statusId: String) { + /** Does nothing for non-cached timelines */ + } + + override suspend fun invalidate() { + currentSource?.invalidate() + } + + override suspend fun translate(status: StatusViewData.Concrete): NetworkResult<Unit> { + status.copy(translation = TranslationViewData.Loading).update() + return timelineCases.translate(status.actionableId) + .map { translation -> + status.copy(translation = TranslationViewData.Loaded(translation)).update() + } + .onFailure { + status.update() + } + } + + override fun untranslate(status: StatusViewData.Concrete) { + status.copy(translation = null).update() + } + + @Throws(IOException::class, HttpException::class) + suspend fun fetchStatusesForKind( + fromId: String?, + uptoId: String?, + limit: Int + ): Response<List<Status>> { + return when (kind) { + Kind.HOME -> api.homeTimeline(maxId = fromId, sinceId = uptoId, limit = limit) + Kind.PUBLIC_FEDERATED -> api.publicTimeline(null, fromId, uptoId, limit) + Kind.PUBLIC_LOCAL -> api.publicTimeline(true, fromId, uptoId, limit) + Kind.TAG -> { + val firstHashtag = tags[0] + val additionalHashtags = tags.subList(1, tags.size) + api.hashtagTimeline(firstHashtag, additionalHashtags, null, fromId, uptoId, limit) + } + + Kind.USER -> api.accountStatuses( + id!!, + fromId, + uptoId, + limit, + excludeReplies = true, + onlyMedia = null, + pinned = null + ) + + Kind.USER_PINNED -> api.accountStatuses( + id!!, + fromId, + uptoId, + limit, + excludeReplies = null, + onlyMedia = null, + pinned = true + ) + + Kind.USER_WITH_REPLIES -> api.accountStatuses( + id!!, + fromId, + uptoId, + limit, + excludeReplies = null, + onlyMedia = null, + pinned = null + ) + + Kind.FAVOURITES -> api.favourites(fromId, uptoId, limit) + Kind.BOOKMARKS -> api.bookmarks(fromId, uptoId, limit) + Kind.LIST -> api.listTimeline(id!!, fromId, uptoId, limit) + Kind.PUBLIC_TRENDING_STATUSES -> api.trendingStatuses(limit = limit, offset = fromId) + } + } + + private fun StatusViewData.Concrete.update() { + val position = + statusData.indexOfFirst { viewData -> viewData.asStatusOrNull()?.id == this.id } + statusData[position] = this + currentSource?.invalidate() + } + + private inline fun updateStatusById( + id: String, + updater: (StatusViewData.Concrete) -> StatusViewData.Concrete + ) { + val pos = statusData.indexOfFirst { it.asStatusOrNull()?.id == id } + if (pos == -1) return + updateViewDataAt(pos, updater) + } + + private inline fun updateActionableStatusById(id: String, updater: (Status) -> Status) { + val pos = statusData.indexOfFirst { it.asStatusOrNull()?.id == id } + if (pos == -1) return + updateViewDataAt(pos) { vd -> + if (vd.status.reblog != null) { + vd.copy(status = vd.status.copy(reblog = updater(vd.status.reblog))) + } else { + vd.copy(status = updater(vd.status)) + } + } + } + + private inline fun updateViewDataAt( + position: Int, + updater: (StatusViewData.Concrete) -> StatusViewData.Concrete + ) { + val status = statusData.getOrNull(position)?.asStatusOrNull() ?: return + statusData[position] = updater(status) + currentSource?.invalidate() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt new file mode 100644 index 0000000..c7a3a38 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/timeline/viewmodel/TimelineViewModel.kt @@ -0,0 +1,305 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.timeline.viewmodel + +import android.content.SharedPreferences +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import at.connyduck.calladapter.networkresult.NetworkResult +import at.connyduck.calladapter.networkresult.fold +import at.connyduck.calladapter.networkresult.getOrElse +import at.connyduck.calladapter.networkresult.getOrThrow +import com.keylesspalace.tusky.appstore.Event +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.FilterUpdatedEvent +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.components.preference.PreferencesFragment.ReadingOrder +import com.keylesspalace.tusky.components.timeline.util.ifExpected +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.FilterV1 +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.network.FilterModel +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.usecase.TimelineCases +import com.keylesspalace.tusky.util.isHttpNotFound +import com.keylesspalace.tusky.viewdata.StatusViewData +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.launch + +abstract class TimelineViewModel( + protected val timelineCases: TimelineCases, + private val api: MastodonApi, + private val eventHub: EventHub, + protected val accountManager: AccountManager, + private val sharedPreferences: SharedPreferences, + private val filterModel: FilterModel +) : ViewModel() { + + abstract val statuses: Flow<PagingData<StatusViewData>> + + var kind: Kind = Kind.HOME + private set + var id: String? = null + private set + var tags: List<String> = emptyList() + private set + + protected var alwaysShowSensitiveMedia = false + private var alwaysOpenSpoilers = false + private var filterRemoveReplies = false + private var filterRemoveReblogs = false + private var filterRemoveSelfReblogs = false + protected var readingOrder: ReadingOrder = ReadingOrder.OLDEST_FIRST + + fun init(kind: Kind, id: String?, tags: List<String>) { + this.kind = kind + this.id = id + this.tags = tags + filterModel.kind = kind.toFilterKind() + + if (kind == Kind.HOME) { + // Note the variable is "true if filter" but the underlying preference/settings text is "true if show" + filterRemoveReplies = + !(accountManager.activeAccount?.isShowHomeReplies ?: true) + filterRemoveReblogs = + !(accountManager.activeAccount?.isShowHomeBoosts ?: true) + filterRemoveSelfReblogs = + !(accountManager.activeAccount?.isShowHomeSelfBoosts ?: true) + } + readingOrder = ReadingOrder.from(sharedPreferences.getString(PrefKeys.READING_ORDER, null)) + + this.alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia + this.alwaysOpenSpoilers = accountManager.activeAccount!!.alwaysOpenSpoiler + + viewModelScope.launch { + eventHub.events + .collect { event -> handleEvent(event) } + } + + reloadFilters() + } + + fun reblog(reblog: Boolean, status: StatusViewData.Concrete): Job = viewModelScope.launch { + try { + timelineCases.reblog(status.actionableId, reblog).getOrThrow() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to reblog status " + status.actionableId, t) + } + } + } + + fun favorite(favorite: Boolean, status: StatusViewData.Concrete): Job = viewModelScope.launch { + try { + timelineCases.favourite(status.actionableId, favorite).getOrThrow() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to favourite status " + status.actionableId, t) + } + } + } + + fun bookmark(bookmark: Boolean, status: StatusViewData.Concrete): Job = viewModelScope.launch { + try { + timelineCases.bookmark(status.actionableId, bookmark).getOrThrow() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to bookmark status " + status.actionableId, t) + } + } + } + + fun voteInPoll(choices: List<Int>, status: StatusViewData.Concrete): Job = + viewModelScope.launch { + val poll = status.status.actionableStatus.poll ?: run { + Log.w(TAG, "No poll on status ${status.id}") + return@launch + } + + val votedPoll = poll.votedCopy(choices) + updatePoll(votedPoll, status) + + try { + timelineCases.voteInPoll(status.actionableId, poll.id, choices).getOrThrow() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to vote in poll: " + status.actionableId, t) + } + } + } + + abstract fun updatePoll(newPoll: Poll, status: StatusViewData.Concrete) + + abstract fun changeExpanded(expanded: Boolean, status: StatusViewData.Concrete) + + abstract fun changeContentShowing(isShowing: Boolean, status: StatusViewData.Concrete) + + abstract fun changeContentCollapsed(isCollapsed: Boolean, status: StatusViewData.Concrete) + + abstract fun removeStatusWithId(id: String) + + abstract fun loadMore(placeholderId: String) + + abstract fun fullReload() + + abstract fun clearWarning(status: StatusViewData.Concrete) + + /** Saves the user's reading position so it can be restored later */ + abstract fun saveReadingPosition(statusId: String) + + /** Triggered when currently displayed data must be reloaded. */ + protected abstract suspend fun invalidate() + + protected fun shouldFilterStatus(statusViewData: StatusViewData): Filter.Action { + val status = statusViewData.asStatusOrNull()?.status ?: return Filter.Action.NONE + return if ( + (status.inReplyToId != null && filterRemoveReplies) || + (status.reblog != null && filterRemoveReblogs) || + ((status.account.id == status.reblog?.account?.id) && filterRemoveSelfReblogs) + ) { + return Filter.Action.HIDE + } else { + statusViewData.filterAction = filterModel.shouldFilterStatus(status.actionableStatus) + statusViewData.filterAction + } + } + + private fun onPreferenceChanged(key: String) { + when (key) { + PrefKeys.TAB_FILTER_HOME_REPLIES -> { + val filter = accountManager.activeAccount?.isShowHomeReplies ?: true + val oldRemoveReplies = filterRemoveReplies + filterRemoveReplies = kind == Kind.HOME && !filter + if (oldRemoveReplies != filterRemoveReplies) { + fullReload() + } + } + PrefKeys.TAB_FILTER_HOME_BOOSTS -> { + val filter = accountManager.activeAccount?.isShowHomeBoosts ?: true + val oldRemoveReblogs = filterRemoveReblogs + filterRemoveReblogs = kind == Kind.HOME && !filter + if (oldRemoveReblogs != filterRemoveReblogs) { + fullReload() + } + } + PrefKeys.TAB_SHOW_HOME_SELF_BOOSTS -> { + val filter = accountManager.activeAccount?.isShowHomeSelfBoosts ?: true + val oldRemoveSelfReblogs = filterRemoveSelfReblogs + filterRemoveSelfReblogs = kind == Kind.HOME && !filter + if (oldRemoveSelfReblogs != filterRemoveSelfReblogs) { + fullReload() + } + } + FilterV1.HOME, FilterV1.NOTIFICATIONS, FilterV1.THREAD, FilterV1.PUBLIC, FilterV1.ACCOUNT -> { + if (filterContextMatchesKind(kind, listOf(key))) { + reloadFilters() + } + } + PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA -> { + // it is ok if only newly loaded statuses are affected, no need to fully refresh + alwaysShowSensitiveMedia = + accountManager.activeAccount!!.alwaysShowSensitiveMedia + } + PrefKeys.READING_ORDER -> { + readingOrder = ReadingOrder.from(sharedPreferences.getString(PrefKeys.READING_ORDER, null)) + } + } + } + + private fun handleEvent(event: Event) { + when (event) { + is PreferenceChangedEvent -> { + onPreferenceChanged(event.preferenceKey) + } + is FilterUpdatedEvent -> { + if (filterContextMatchesKind(kind, event.filterContext)) { + fullReload() + } + } + } + } + + private fun reloadFilters() { + viewModelScope.launch { + api.getFilters().fold( + { + // After the filters are loaded we need to reload displayed content to apply them. + // It can happen during the usage or at startup, when we get statuses before filters. + invalidate() + }, + { throwable -> + if (throwable.isHttpNotFound()) { + // Fallback to client-side filter code + val filters = api.getFiltersV1().getOrElse { + Log.e(TAG, "Failed to fetch filters", it) + return@launch + } + filterModel.initWithFilters( + filters.filter { + filterContextMatchesKind(kind, it.context) + } + ) + // After the filters are loaded we need to reload displayed content to apply them. + // It can happen during the usage or at startup, when we get statuses before filters. + invalidate() + } else { + Log.e(TAG, "Error getting filters", throwable) + } + } + ) + } + } + + abstract suspend fun translate(status: StatusViewData.Concrete): NetworkResult<Unit> + abstract fun untranslate(status: StatusViewData.Concrete) + + companion object { + private const val TAG = "TimelineVM" + internal const val LOAD_AT_ONCE = 30 + + fun filterContextMatchesKind(kind: Kind, filterContext: List<String>): Boolean { + return filterContext.contains(kind.toFilterKind().kind) + } + } + + enum class Kind { + HOME, + PUBLIC_LOCAL, + PUBLIC_FEDERATED, + TAG, + USER, + USER_PINNED, + USER_WITH_REPLIES, + FAVOURITES, + LIST, + BOOKMARKS, + PUBLIC_TRENDING_STATUSES; + + fun toFilterKind(): Filter.Kind { + return when (valueOf(name)) { + HOME, LIST -> Filter.Kind.HOME + PUBLIC_FEDERATED, PUBLIC_LOCAL, TAG, FAVOURITES, PUBLIC_TRENDING_STATUSES -> Filter.Kind.PUBLIC + USER, USER_WITH_REPLIES, USER_PINNED -> Filter.Kind.ACCOUNT + else -> Filter.Kind.PUBLIC + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingActivity.kt new file mode 100644 index 0000000..6e205a2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingActivity.kt @@ -0,0 +1,56 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <https://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.trending + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.commit +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ActivityTrendingBinding +import com.keylesspalace.tusky.util.viewBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class TrendingActivity : BaseActivity() { + + private val binding: ActivityTrendingBinding by viewBinding(ActivityTrendingBinding::inflate) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + + setSupportActionBar(binding.includedToolbar.toolbar) + + supportActionBar?.run { + setTitle(R.string.title_public_trending_hashtags) + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + } + + if (supportFragmentManager.findFragmentById(R.id.fragmentContainer) == null) { + supportFragmentManager.commit { + val fragment = TrendingTagsFragment.newInstance() + replace(R.id.fragmentContainer, fragment) + } + } + } + + companion object { + fun getIntent(context: Context) = Intent(context, TrendingActivity::class.java) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingDateViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingDateViewHolder.kt new file mode 100644 index 0000000..5d9e3c3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingDateViewHolder.kt @@ -0,0 +1,41 @@ +/* Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.trending + +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemTrendingDateBinding +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +class TrendingDateViewHolder( + private val binding: ItemTrendingDateBinding +) : RecyclerView.ViewHolder(binding.root) { + + private val dateFormat = SimpleDateFormat("EEE dd MMM yyyy", Locale.getDefault()).apply { + this.timeZone = TimeZone.getDefault() + } + + fun setup(start: Date, end: Date) { + binding.dates.text = itemView.context.getString( + R.string.date_range, + dateFormat.format(start), + dateFormat.format(end) + ) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagViewHolder.kt b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagViewHolder.kt new file mode 100644 index 0000000..ac1e8c7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagViewHolder.kt @@ -0,0 +1,54 @@ +/* Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.trending + +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ItemTrendingCellBinding +import com.keylesspalace.tusky.util.formatNumber +import com.keylesspalace.tusky.viewdata.TrendingViewData + +class TrendingTagViewHolder( + private val binding: ItemTrendingCellBinding +) : RecyclerView.ViewHolder(binding.root) { + + fun setup(tagViewData: TrendingViewData.Tag, onViewTag: (String) -> Unit) { + binding.tag.text = binding.root.context.getString(R.string.title_tag, tagViewData.name) + + binding.graph.maxTrendingValue = tagViewData.maxTrendingValue + binding.graph.primaryLineData = tagViewData.usage + binding.graph.secondaryLineData = tagViewData.accounts + + binding.totalUsage.text = formatNumber(tagViewData.usage.sum(), 1000) + + val totalAccounts = tagViewData.accounts.sum() + binding.totalAccounts.text = formatNumber(totalAccounts, 1000) + + binding.currentUsage.text = tagViewData.usage.last().toString() + binding.currentAccounts.text = tagViewData.usage.last().toString() + + itemView.setOnClickListener { + onViewTag(tagViewData.name) + } + + itemView.contentDescription = + itemView.context.getString( + R.string.accessibility_talking_about_tag, + totalAccounts, + tagViewData.name + ) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagsAdapter.kt new file mode 100644 index 0000000..4137d20 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagsAdapter.kt @@ -0,0 +1,92 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.trending + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.databinding.ItemTrendingCellBinding +import com.keylesspalace.tusky.databinding.ItemTrendingDateBinding +import com.keylesspalace.tusky.viewdata.TrendingViewData + +class TrendingTagsAdapter( + private val onViewTag: (String) -> Unit +) : ListAdapter<TrendingViewData, RecyclerView.ViewHolder>(TrendingDifferCallback) { + + init { + stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY + } + + override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + return when (viewType) { + VIEW_TYPE_TAG -> { + val binding = + ItemTrendingCellBinding.inflate(LayoutInflater.from(viewGroup.context)) + TrendingTagViewHolder(binding) + } + else -> { + val binding = + ItemTrendingDateBinding.inflate(LayoutInflater.from(viewGroup.context)) + TrendingDateViewHolder(binding) + } + } + } + + override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) { + when (val viewData = getItem(position)) { + is TrendingViewData.Tag -> { + val holder = viewHolder as TrendingTagViewHolder + holder.setup(viewData, onViewTag) + } + + is TrendingViewData.Header -> { + val holder = viewHolder as TrendingDateViewHolder + holder.setup(viewData.start, viewData.end) + } + } + } + + override fun getItemViewType(position: Int): Int { + return if (getItem(position) is TrendingViewData.Tag) { + VIEW_TYPE_TAG + } else { + VIEW_TYPE_HEADER + } + } + + companion object { + const val VIEW_TYPE_HEADER = 0 + const val VIEW_TYPE_TAG = 1 + + val TrendingDifferCallback = object : DiffUtil.ItemCallback<TrendingViewData>() { + override fun areItemsTheSame( + oldItem: TrendingViewData, + newItem: TrendingViewData + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: TrendingViewData, + newItem: TrendingViewData + ): Boolean { + return oldItem == newItem + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagsFragment.kt new file mode 100644 index 0000000..b6c1956 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/trending/TrendingTagsFragment.kt @@ -0,0 +1,259 @@ +/* Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.trending + +import android.content.res.Configuration +import android.os.Bundle +import android.util.Log +import android.view.View +import android.view.accessibility.AccessibilityManager +import androidx.core.content.getSystemService +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.GridLayoutManager.SpanSizeLookup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.SimpleItemAnimator +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import at.connyduck.sparkbutton.helpers.Utils +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.StatusListActivity +import com.keylesspalace.tusky.components.trending.viewmodel.TrendingTagsViewModel +import com.keylesspalace.tusky.databinding.FragmentTrendingTagsBinding +import com.keylesspalace.tusky.interfaces.ActionButtonActivity +import com.keylesspalace.tusky.interfaces.RefreshableFragment +import com.keylesspalace.tusky.interfaces.ReselectableFragment +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.viewdata.TrendingViewData +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class TrendingTagsFragment : + Fragment(R.layout.fragment_trending_tags), + OnRefreshListener, + ReselectableFragment, + RefreshableFragment { + + private val viewModel: TrendingTagsViewModel by viewModels() + + private val binding by viewBinding(FragmentTrendingTagsBinding::bind) + + private var adapter: TrendingTagsAdapter? = null + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + val columnCount = + requireContext().resources.getInteger(R.integer.trending_column_count) + adapter?.let { + setupLayoutManager(it, columnCount) + } + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + val adapter = TrendingTagsAdapter(::onViewTag) + this.adapter = adapter + binding.swipeRefreshLayout.setOnRefreshListener(this) + setupRecyclerView(adapter) + + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + val firstPos = (binding.recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition() + if (firstPos == 0 && positionStart == 0 && adapter.itemCount != itemCount) { + binding.recyclerView.post { + if (getView() != null) { + binding.recyclerView.scrollBy( + 0, + Utils.dpToPx(requireContext(), -30) + ) + } + } + } + } + }) + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.uiState.collectLatest { trendingState -> + processViewState(adapter, trendingState) + } + } + + (requireActivity() as? ActionButtonActivity)?.actionButton?.visibility = View.GONE + } + + override fun onDestroyView() { + // Clear the adapter to prevent leaking the View + adapter = null + super.onDestroyView() + } + + private fun setupLayoutManager(adapter: TrendingTagsAdapter, columnCount: Int) { + binding.recyclerView.layoutManager = GridLayoutManager(context, columnCount).apply { + spanSizeLookup = object : SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return when (adapter.getItemViewType(position)) { + TrendingTagsAdapter.VIEW_TYPE_HEADER -> columnCount + TrendingTagsAdapter.VIEW_TYPE_TAG -> 1 + else -> -1 + } + } + } + } + } + + private fun setupRecyclerView(adapter: TrendingTagsAdapter) { + val columnCount = + requireContext().resources.getInteger(R.integer.trending_column_count) + setupLayoutManager(adapter, columnCount) + + binding.recyclerView.setHasFixedSize(true) + + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + binding.recyclerView.adapter = adapter + } + + override fun onRefresh() { + viewModel.invalidate(true) + } + + fun onViewTag(tag: String) { + requireActivity().startActivityWithSlideInAnimation( + StatusListActivity.newHashtagIntent(requireContext(), tag) + ) + } + + private fun processViewState( + adapter: TrendingTagsAdapter, + uiState: TrendingTagsViewModel.TrendingTagsUiState + ) { + Log.d(TAG, uiState.loadingState.name) + when (uiState.loadingState) { + TrendingTagsViewModel.LoadingState.INITIAL -> clearLoadingState() + TrendingTagsViewModel.LoadingState.LOADING -> applyLoadingState() + TrendingTagsViewModel.LoadingState.REFRESHING -> applyRefreshingState() + TrendingTagsViewModel.LoadingState.LOADED -> applyLoadedState(adapter, uiState.trendingViewData) + TrendingTagsViewModel.LoadingState.ERROR_NETWORK -> networkError() + TrendingTagsViewModel.LoadingState.ERROR_OTHER -> otherError() + } + } + + private fun applyLoadedState(adapter: TrendingTagsAdapter, viewData: List<TrendingViewData>) { + clearLoadingState() + + adapter.submitList(viewData) + + if (viewData.isEmpty()) { + binding.recyclerView.hide() + binding.messageView.show() + binding.messageView.setup( + R.drawable.elephant_friend_empty, + R.string.message_empty, + null + ) + } else { + binding.recyclerView.show() + binding.messageView.hide() + } + binding.progressBar.hide() + } + + private fun applyRefreshingState() { + binding.swipeRefreshLayout.isRefreshing = true + } + + private fun applyLoadingState() { + binding.recyclerView.hide() + binding.messageView.hide() + binding.progressBar.show() + } + + private fun clearLoadingState() { + binding.swipeRefreshLayout.isRefreshing = false + binding.progressBar.hide() + binding.messageView.hide() + } + + private fun networkError() { + binding.recyclerView.hide() + binding.messageView.show() + binding.progressBar.hide() + + binding.swipeRefreshLayout.isRefreshing = false + binding.messageView.setup( + R.drawable.errorphant_offline, + R.string.error_network + ) { refreshContent() } + } + + private fun otherError() { + binding.recyclerView.hide() + binding.messageView.show() + binding.progressBar.hide() + + binding.swipeRefreshLayout.isRefreshing = false + binding.messageView.setup( + R.drawable.errorphant_error, + R.string.error_generic + ) { refreshContent() } + } + + private fun actionButtonPresent(): Boolean { + return activity is ActionButtonActivity + } + + private var talkBackWasEnabled = false + + override fun onResume() { + super.onResume() + val a11yManager = requireContext().getSystemService<AccessibilityManager>() + + val wasEnabled = talkBackWasEnabled + talkBackWasEnabled = a11yManager?.isEnabled == true + Log.d(TAG, "talkback was enabled: $wasEnabled, now $talkBackWasEnabled") + if (talkBackWasEnabled && !wasEnabled) { + val adapter = requireNotNull(this.adapter) + adapter.notifyItemRangeChanged(0, adapter.itemCount) + } + + if (actionButtonPresent()) { + val composeButton = (activity as ActionButtonActivity).actionButton + composeButton?.hide() + } + } + + override fun onReselect() { + if (view != null) { + binding.recyclerView.layoutManager?.scrollToPosition(0) + binding.recyclerView.stopScroll() + } + } + + override fun refreshContent() { + onRefresh() + } + + companion object { + private const val TAG = "TrendingTagsFragment" + + fun newInstance() = TrendingTagsFragment() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/trending/viewmodel/TrendingTagsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/trending/viewmodel/TrendingTagsViewModel.kt new file mode 100644 index 0000000..ea8b4b0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/trending/viewmodel/TrendingTagsViewModel.kt @@ -0,0 +1,127 @@ +/* Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.trending.viewmodel + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.end +import com.keylesspalace.tusky.entity.start +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.toViewData +import com.keylesspalace.tusky.viewdata.TrendingViewData +import dagger.hilt.android.lifecycle.HiltViewModel +import java.io.IOException +import javax.inject.Inject +import kotlinx.coroutines.async +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.launch + +@HiltViewModel +class TrendingTagsViewModel @Inject constructor( + private val mastodonApi: MastodonApi, + private val eventHub: EventHub +) : ViewModel() { + enum class LoadingState { + INITIAL, + LOADING, + REFRESHING, + LOADED, + ERROR_NETWORK, + ERROR_OTHER + } + + data class TrendingTagsUiState( + val trendingViewData: List<TrendingViewData>, + val loadingState: LoadingState + ) + + val uiState: Flow<TrendingTagsUiState> get() = _uiState + private val _uiState = MutableStateFlow(TrendingTagsUiState(listOf(), LoadingState.INITIAL)) + + init { + invalidate() + + // Collect PreferenceChangedEvent, FiltersActivity creates them when a filter is created + // or deleted. Unfortunately, there's nothing in the event to determine if it's a filter + // that was modified, so refresh on every preference change. + viewModelScope.launch { + eventHub.events + .filterIsInstance<PreferenceChangedEvent>() + .collect { + invalidate() + } + } + } + + /** + * Invalidate the current list of trending tags and fetch a new list. + * + * A tag is excluded if it is filtered by the user on their home timeline. + */ + fun invalidate(refresh: Boolean = false) = viewModelScope.launch { + if (refresh) { + _uiState.value = TrendingTagsUiState(emptyList(), LoadingState.REFRESHING) + } else { + _uiState.value = TrendingTagsUiState(emptyList(), LoadingState.LOADING) + } + + val deferredFilters = async { mastodonApi.getFilters() } + + mastodonApi.trendingTags().fold( + { tagResponse -> + + val firstTag = tagResponse.firstOrNull() + _uiState.value = if (firstTag == null) { + TrendingTagsUiState(emptyList(), LoadingState.LOADED) + } else { + val homeFilters = deferredFilters.await().getOrNull()?.filter { filter -> + filter.context.contains(Filter.Kind.HOME.kind) + } + val tags = tagResponse + .filter { tag -> + homeFilters?.none { filter -> + filter.keywords.any { keyword -> keyword.keyword.equals(tag.name, ignoreCase = true) } + } ?: false + } + .sortedByDescending { tag -> tag.history.sumOf { it.uses.toLongOrNull() ?: 0 } } + .toViewData() + + val header = TrendingViewData.Header(firstTag.start, firstTag.end) + TrendingTagsUiState(listOf(header) + tags, LoadingState.LOADED) + } + }, + { error -> + Log.w(TAG, "failed loading trending tags", error) + if (error is IOException) { + _uiState.value = TrendingTagsUiState(emptyList(), LoadingState.ERROR_NETWORK) + } else { + _uiState.value = TrendingTagsUiState(emptyList(), LoadingState.ERROR_OTHER) + } + } + ) + } + + companion object { + private const val TAG = "TrendingViewModel" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ConversationLineItemDecoration.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ConversationLineItemDecoration.kt new file mode 100644 index 0000000..520445f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ConversationLineItemDecoration.kt @@ -0,0 +1,78 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.viewthread + +import android.content.Context +import android.graphics.Canvas +import android.graphics.drawable.Drawable +import android.view.View +import androidx.core.content.ContextCompat +import androidx.core.view.forEach +import androidx.recyclerview.widget.RecyclerView +import com.keylesspalace.tusky.R + +class ConversationLineItemDecoration(context: Context) : RecyclerView.ItemDecoration() { + private val divider: Drawable = ContextCompat.getDrawable( + context, + R.drawable.conversation_thread_line + )!! + + private val avatarTopMargin = context.resources.getDimensionPixelSize( + R.dimen.account_avatar_margin + ) + private val halfAvatarHeight = context.resources.getDimensionPixelSize(R.dimen.timeline_status_avatar_height) / 2 + private val statusLineMarginStart = context.resources.getDimensionPixelSize( + R.dimen.status_line_margin_start + ) + + override fun onDraw(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) { + val dividerStart = parent.paddingStart + statusLineMarginStart + val dividerEnd = dividerStart + divider.intrinsicWidth + + val items = (parent.adapter as ThreadAdapter).currentList + + parent.forEach { statusItemView -> + val position = parent.getChildAdapterPosition(statusItemView) + + items.getOrNull(position)?.let { current -> + val above = items.getOrNull(position - 1) + val dividerTop = if (above != null && above.id == current.status.inReplyToId) { + statusItemView.top + } else { + statusItemView.top + avatarTopMargin + halfAvatarHeight + } + val below = items.getOrNull(position + 1) + val dividerBottom = if (below != null && current.id == below.status.inReplyToId && !current.isDetailed) { + statusItemView.bottom + } else { + statusItemView.top + avatarTopMargin + halfAvatarHeight + } + + if (parent.layoutDirection == View.LAYOUT_DIRECTION_LTR) { + divider.setBounds(dividerStart, dividerTop, dividerEnd, dividerBottom) + } else { + divider.setBounds( + canvas.width - dividerEnd, + dividerTop, + canvas.width - dividerStart, + dividerBottom + ) + } + divider.draw(canvas) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt new file mode 100644 index 0000000..0c1c7fc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ThreadAdapter.kt @@ -0,0 +1,105 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.viewthread + +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.adapter.StatusDetailedViewHolder +import com.keylesspalace.tusky.adapter.StatusViewHolder +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.viewdata.StatusViewData + +class ThreadAdapter( + private val statusDisplayOptions: StatusDisplayOptions, + private val statusActionListener: StatusActionListener +) : ListAdapter<StatusViewData.Concrete, StatusBaseViewHolder>(ThreadDifferCallback) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StatusBaseViewHolder { + val inflater = LayoutInflater.from(parent.context) + return when (viewType) { + VIEW_TYPE_STATUS -> { + StatusViewHolder(inflater.inflate(R.layout.item_status, parent, false)) + } + VIEW_TYPE_STATUS_FILTERED -> { + StatusViewHolder(inflater.inflate(R.layout.item_status_wrapper, parent, false)) + } + VIEW_TYPE_STATUS_DETAILED -> { + StatusDetailedViewHolder( + inflater.inflate(R.layout.item_status_detailed, parent, false) + ) + } + else -> error("Unknown item type: $viewType") + } + } + + override fun onBindViewHolder(viewHolder: StatusBaseViewHolder, position: Int) { + val status = getItem(position) + viewHolder.setupWithStatus(status, statusActionListener, statusDisplayOptions) + } + + override fun getItemViewType(position: Int): Int { + val viewData = getItem(position) + return if (viewData.isDetailed) { + VIEW_TYPE_STATUS_DETAILED + } else if (viewData.filterAction == Filter.Action.WARN) { + VIEW_TYPE_STATUS_FILTERED + } else { + VIEW_TYPE_STATUS + } + } + + companion object { + private const val TAG = "ThreadAdapter" + private const val VIEW_TYPE_STATUS = 0 + private const val VIEW_TYPE_STATUS_DETAILED = 1 + private const val VIEW_TYPE_STATUS_FILTERED = 2 + + val ThreadDifferCallback = object : DiffUtil.ItemCallback<StatusViewData.Concrete>() { + override fun areItemsTheSame( + oldItem: StatusViewData.Concrete, + newItem: StatusViewData.Concrete + ): Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame( + oldItem: StatusViewData.Concrete, + newItem: StatusViewData.Concrete + ): Boolean { + return false // Items are different always. It allows to refresh timestamp on every view holder update + } + + override fun getChangePayload( + oldItem: StatusViewData.Concrete, + newItem: StatusViewData.Concrete + ): Any? { + return if (oldItem == newItem) { + // If items are equal - update timestamp only + listOf(StatusBaseViewHolder.Key.KEY_CREATED) + } else { + // If items are different - update the whole view holder + null + } + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadActivity.kt new file mode 100644 index 0000000..5227fb2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadActivity.kt @@ -0,0 +1,66 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.viewthread + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import androidx.fragment.app.commit +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ActivityViewThreadBinding +import com.keylesspalace.tusky.util.viewBinding +import dagger.hilt.android.AndroidEntryPoint + +@AndroidEntryPoint +class ViewThreadActivity : BottomSheetActivity() { + + private val binding by viewBinding(ActivityViewThreadBinding::inflate) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(binding.root) + setSupportActionBar(binding.toolbar) + supportActionBar?.run { + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + setDisplayShowTitleEnabled(true) + } + val id = intent.getStringExtra(ID_EXTRA)!! + val url = intent.getStringExtra(URL_EXTRA)!! + val fragment = + supportFragmentManager.findFragmentByTag(FRAGMENT_TAG + id) as ViewThreadFragment? + ?: ViewThreadFragment.newInstance(id, url) + + supportFragmentManager.commit { + replace(R.id.fragment_container, fragment, FRAGMENT_TAG + id) + } + } + + companion object { + + fun startIntent(context: Context, id: String, url: String): Intent { + val intent = Intent(context, ViewThreadActivity::class.java) + intent.putExtra(ID_EXTRA, id) + intent.putExtra(URL_EXTRA, url) + return intent + } + + private const val ID_EXTRA = "id" + private const val URL_EXTRA = "url" + private const val FRAGMENT_TAG = "ViewThreadFragment_" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt new file mode 100644 index 0000000..e19be4b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadFragment.kt @@ -0,0 +1,509 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.viewthread + +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.widget.LinearLayout +import androidx.annotation.CheckResult +import androidx.core.view.MenuProvider +import androidx.fragment.app.commit +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.SimpleItemAnimator +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import at.connyduck.calladapter.networkresult.onFailure +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.accountlist.AccountListActivity +import com.keylesspalace.tusky.components.accountlist.AccountListActivity.Companion.newIntent +import com.keylesspalace.tusky.components.viewthread.edits.ViewEditsFragment +import com.keylesspalace.tusky.databinding.FragmentViewThreadBinding +import com.keylesspalace.tusky.fragment.SFragment +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.CardViewMode +import com.keylesspalace.tusky.util.ListStatusAccessibilityDelegate +import com.keylesspalace.tusky.util.StatusDisplayOptions +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.openLink +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.viewdata.AttachmentViewData.Companion.list +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.keylesspalace.tusky.viewdata.TranslationViewData +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class ViewThreadFragment : + SFragment(R.layout.fragment_view_thread), + OnRefreshListener, + StatusActionListener, + MenuProvider { + + @Inject + lateinit var preferences: SharedPreferences + + private val viewModel: ViewThreadViewModel by viewModels() + + private val binding by viewBinding(FragmentViewThreadBinding::bind) + + private var adapter: ThreadAdapter? = null + private lateinit var thisThreadsStatusId: String + + private var alwaysShowSensitiveMedia = false + private var alwaysOpenSpoiler = false + + /** + * State of the "reveal" menu item that shows/hides content that is behind a content + * warning. Setting this invalidates the menu to redraw the menu item. + */ + private var revealButtonState = RevealButtonState.NO_BUTTON + set(value) { + field = value + requireActivity().invalidateMenu() + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + thisThreadsStatusId = requireArguments().getString(ID_EXTRA)!! + } + + private fun createAdapter(): ThreadAdapter { + val statusDisplayOptions = StatusDisplayOptions( + animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + mediaPreviewEnabled = accountManager.activeAccount!!.mediaPreviewEnabled, + useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false), + showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true), + useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true), + cardViewMode = if (preferences.getBoolean(PrefKeys.SHOW_CARDS_IN_TIMELINES, false)) { + CardViewMode.INDENTED + } else { + CardViewMode.NONE + }, + confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true), + confirmFavourites = preferences.getBoolean(PrefKeys.CONFIRM_FAVOURITES, false), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), + showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false), + showSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia, + openSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler + ) + return ThreadAdapter(statusDisplayOptions, this) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) + + val adapter = createAdapter() + this.adapter = adapter + + binding.swipeRefreshLayout.setOnRefreshListener(this) + + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(context) + binding.recyclerView.setAccessibilityDelegateCompat( + ListStatusAccessibilityDelegate( + binding.recyclerView, + this + ) { index -> adapter.currentList.getOrNull(index) } + ) + val divider = DividerItemDecoration(context, LinearLayout.VERTICAL) + binding.recyclerView.addItemDecoration(divider) + binding.recyclerView.addItemDecoration(ConversationLineItemDecoration(requireContext())) + alwaysShowSensitiveMedia = accountManager.activeAccount!!.alwaysShowSensitiveMedia + alwaysOpenSpoiler = accountManager.activeAccount!!.alwaysOpenSpoiler + + binding.recyclerView.adapter = adapter + + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + + var initialProgressBar = getProgressBarJob(binding.initialProgressBar, 500) + var threadProgressBar = getProgressBarJob(binding.threadProgressBar, 500) + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.uiState.collect { uiState -> + when (uiState) { + is ThreadUiState.Loading -> { + revealButtonState = RevealButtonState.NO_BUTTON + + binding.recyclerView.hide() + binding.statusView.hide() + + initialProgressBar = getProgressBarJob(binding.initialProgressBar, 500) + initialProgressBar.start() + } + + is ThreadUiState.LoadingThread -> { + if (uiState.statusViewDatum == null) { + // no detailed statuses available, e.g. because author is blocked + activity?.finish() + return@collect + } + + initialProgressBar.cancel() + threadProgressBar = getProgressBarJob(binding.threadProgressBar, 500) + threadProgressBar.start() + + if (viewModel.isInitialLoad) { + adapter.submitList(listOf(uiState.statusViewDatum)) + + // else this "submit one and then all on success below" will always center on the one + } + + revealButtonState = uiState.revealButton + binding.swipeRefreshLayout.isRefreshing = false + + binding.recyclerView.show() + binding.statusView.hide() + } + + is ThreadUiState.Error -> { + Log.w(TAG, "failed to load status", uiState.throwable) + initialProgressBar.cancel() + threadProgressBar.cancel() + + revealButtonState = RevealButtonState.NO_BUTTON + binding.swipeRefreshLayout.isRefreshing = false + + binding.recyclerView.hide() + binding.statusView.show() + + binding.statusView.setup( + uiState.throwable + ) { viewModel.retry(thisThreadsStatusId) } + } + + is ThreadUiState.Success -> { + if (uiState.statusViewData.none { viewData -> viewData.isDetailed }) { + // no detailed statuses available, e.g. because author is blocked + activity?.finish() + return@collect + } + + threadProgressBar.cancel() + + adapter.submitList(uiState.statusViewData) { + if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) && viewModel.isInitialLoad) { + viewModel.isInitialLoad = false + + // Ensure the top of the status is visible + (binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPositionWithOffset( + uiState.detailedStatusPosition, + 0 + ) + } + } + + revealButtonState = uiState.revealButton + binding.swipeRefreshLayout.isRefreshing = false + + binding.recyclerView.show() + binding.statusView.hide() + } + + is ThreadUiState.Refreshing -> { + threadProgressBar.cancel() + } + } + } + } + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.errors.collect { throwable -> + Log.w(TAG, "failed to load status context", throwable) + Snackbar.make(binding.root, R.string.error_generic, Snackbar.LENGTH_SHORT) + .setAction(R.string.action_retry) { + viewModel.retry(thisThreadsStatusId) + } + .show() + } + } + + viewModel.loadThread(thisThreadsStatusId) + } + + override fun onDestroyView() { + // Clear the adapter to prevent leaking the View + adapter = null + super.onDestroyView() + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.fragment_view_thread, menu) + val actionReveal = menu.findItem(R.id.action_reveal) + actionReveal.isVisible = revealButtonState != RevealButtonState.NO_BUTTON + actionReveal.setIcon( + when (revealButtonState) { + RevealButtonState.REVEAL -> R.drawable.ic_eye_24dp + else -> R.drawable.ic_hide_media_24dp + } + ) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_reveal -> { + viewModel.toggleRevealButton() + true + } + + R.id.action_open_in_web -> { + context?.openLink(requireArguments().getString(URL_EXTRA)!!) + true + } + + R.id.action_refresh -> { + onRefresh() + true + } + + else -> false + } + } + + override fun onResume() { + super.onResume() + requireActivity().title = getString(R.string.title_view_thread) + } + + /** + * Create a job to implement a delayed-visible progress bar. + * + * Delaying the visibility of the progress bar can improve user perception of UI speed because + * fewer UI elements are appearing and disappearing. + * + * When started the job will wait `delayMs` then show `view`. If the job is cancelled at + * any time `view` is hidden. + */ + @CheckResult + private fun getProgressBarJob(view: View, delayMs: Long) = + viewLifecycleOwner.lifecycleScope.launch( + start = CoroutineStart.LAZY + ) { + try { + delay(delayMs) + view.show() + awaitCancellation() + } finally { + view.hide() + } + } + + override fun onRefresh() { + viewModel.refresh(thisThreadsStatusId) + } + + override fun onReply(position: Int) { + val viewData = adapter?.currentList?.getOrNull(position) ?: return + super.reply(viewData.status) + } + + override fun onReblog(reblog: Boolean, position: Int) { + val status = adapter?.currentList?.getOrNull(position) ?: return + viewModel.reblog(reblog, status) + } + + override val onMoreTranslate: ((translate: Boolean, position: Int) -> Unit) = + { translate: Boolean, position: Int -> + if (translate) { + onTranslate(position) + } else { + onUntranslate( + position + ) + } + } + + private fun onTranslate(position: Int) { + val status = adapter?.currentList?.getOrNull(position) ?: return + viewLifecycleOwner.lifecycleScope.launch { + viewModel.translate(status) + .onFailure { + Snackbar.make( + requireView(), + getString(R.string.ui_error_translate, it.message), + Snackbar.LENGTH_LONG + ).show() + } + } + } + + override fun onUntranslate(position: Int) { + val status = adapter?.currentList?.getOrNull(position) ?: return + viewModel.untranslate(status) + } + + override fun onFavourite(favourite: Boolean, position: Int) { + val status = adapter?.currentList?.getOrNull(position) ?: return + viewModel.favorite(favourite, status) + } + + override fun onBookmark(bookmark: Boolean, position: Int) { + val status = adapter?.currentList?.getOrNull(position) ?: return + viewModel.bookmark(bookmark, status) + } + + override fun onMore(view: View, position: Int) { + val viewData = adapter?.currentList?.getOrNull(position) ?: return + super.more( + viewData.status, + view, + position, + (viewData.translation as? TranslationViewData.Loaded)?.data + ) + } + + override fun onViewMedia(position: Int, attachmentIndex: Int, view: View?) { + val status = adapter?.currentList?.getOrNull(position) ?: return + super.viewMedia( + attachmentIndex, + list(status, alwaysShowSensitiveMedia), + view + ) + } + + override fun onViewThread(position: Int) { + val status = adapter?.currentList?.getOrNull(position) ?: return + if (thisThreadsStatusId == status.id) { + // If already viewing this thread, don't reopen it. + return + } + super.viewThread(status.actionableId, status.actionable.url) + } + + override fun onViewUrl(url: String) { + val status: StatusViewData.Concrete? = viewModel.detailedStatus() + if (status != null && status.status.url == url) { + // already viewing the status with this url + // probably just a preview federated and the user is clicking again to view more -> open the browser + // this can happen with some friendica statuses + requireContext().openLink(url) + return + } + super.onViewUrl(url) + } + + override fun onOpenReblog(position: Int) { + // there are no reblogs in threads + } + + override fun onExpandedChange(expanded: Boolean, position: Int) { + val status = adapter?.currentList?.getOrNull(position) ?: return + viewModel.changeExpanded(expanded, status) + } + + override fun onContentHiddenChange(isShowing: Boolean, position: Int) { + val status = adapter?.currentList?.getOrNull(position) ?: return + viewModel.changeContentShowing(isShowing, status) + } + + override fun onLoadMore(position: Int) { + // only used in timelines + } + + override fun onShowReblogs(position: Int) { + val status = adapter?.currentList?.getOrNull(position) ?: return + val intent = newIntent(requireContext(), AccountListActivity.Type.REBLOGGED, status.id) + requireActivity().startActivityWithSlideInAnimation(intent) + } + + override fun onShowFavs(position: Int) { + val status = adapter?.currentList?.getOrNull(position) ?: return + val intent = newIntent(requireContext(), AccountListActivity.Type.FAVOURITED, status.id) + requireActivity().startActivityWithSlideInAnimation(intent) + } + + override fun onContentCollapsedChange(isCollapsed: Boolean, position: Int) { + val status = adapter?.currentList?.getOrNull(position) ?: return + viewModel.changeContentCollapsed(isCollapsed, status) + } + + override fun onViewTag(tag: String) { + super.viewTag(tag) + } + + override fun onViewAccount(id: String) { + super.viewAccount(id) + } + + public override fun removeItem(position: Int) { + adapter?.currentList?.getOrNull(position)?.let { status -> + if (status.isDetailed) { + // the main status we are viewing is being removed, finish the activity + activity?.finish() + return + } + viewModel.removeStatus(status) + } + } + + override fun onVoteInPoll(position: Int, choices: List<Int>) { + val status = adapter?.currentList?.getOrNull(position) ?: return + viewModel.voteInPoll(choices, status) + } + + override fun onShowEdits(position: Int) { + val status = adapter?.currentList?.getOrNull(position) ?: return + val viewEditsFragment = ViewEditsFragment.newInstance(status.actionableId) + + parentFragmentManager.commit { + setCustomAnimations( + R.anim.activity_open_enter, + R.anim.activity_open_exit, + R.anim.activity_close_enter, + R.anim.activity_close_exit + ) + replace(R.id.fragment_container, viewEditsFragment, "ViewEditsFragment_$id") + addToBackStack(null) + } + } + + override fun clearWarningAction(position: Int) { + val status = adapter?.currentList?.getOrNull(position) ?: return + viewModel.clearWarning(status) + } + + companion object { + private const val TAG = "ViewThreadFragment" + + private const val ID_EXTRA = "id" + private const val URL_EXTRA = "url" + + fun newInstance(id: String, url: String): ViewThreadFragment { + val arguments = Bundle(2) + val fragment = ViewThreadFragment() + arguments.putString(ID_EXTRA, id) + arguments.putString(URL_EXTRA, url) + fragment.arguments = arguments + return fragment + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt new file mode 100644 index 0000000..7934f1c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModel.kt @@ -0,0 +1,557 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.viewthread + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.NetworkResult +import at.connyduck.calladapter.networkresult.fold +import at.connyduck.calladapter.networkresult.getOrElse +import at.connyduck.calladapter.networkresult.getOrThrow +import at.connyduck.calladapter.networkresult.map +import at.connyduck.calladapter.networkresult.onFailure +import at.connyduck.calladapter.networkresult.onSuccess +import com.keylesspalace.tusky.appstore.BlockEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.StatusChangedEvent +import com.keylesspalace.tusky.appstore.StatusComposedEvent +import com.keylesspalace.tusky.appstore.StatusDeletedEvent +import com.keylesspalace.tusky.components.timeline.toStatus +import com.keylesspalace.tusky.components.timeline.util.ifExpected +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.FilterV1 +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.FilterModel +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.usecase.TimelineCases +import com.keylesspalace.tusky.util.isHttpNotFound +import com.keylesspalace.tusky.util.toViewData +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.keylesspalace.tusky.viewdata.TranslationViewData +import com.squareup.moshi.Moshi +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Job +import kotlinx.coroutines.async +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +@HiltViewModel +class ViewThreadViewModel @Inject constructor( + private val api: MastodonApi, + private val filterModel: FilterModel, + private val timelineCases: TimelineCases, + eventHub: EventHub, + private val accountManager: AccountManager, + private val db: AppDatabase, + private val moshi: Moshi +) : ViewModel() { + + private val _uiState = MutableStateFlow(ThreadUiState.Loading as ThreadUiState) + val uiState: Flow<ThreadUiState> = _uiState.asStateFlow() + + private val _errors = MutableSharedFlow<Throwable>( + replay = 0, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val errors: SharedFlow<Throwable> = _errors.asSharedFlow() + + var isInitialLoad: Boolean = true + + private val alwaysShowSensitiveMedia: Boolean + private val alwaysOpenSpoiler: Boolean + + init { + val activeAccount = accountManager.activeAccount + alwaysShowSensitiveMedia = activeAccount?.alwaysShowSensitiveMedia ?: false + alwaysOpenSpoiler = activeAccount?.alwaysOpenSpoiler ?: false + + viewModelScope.launch { + eventHub.events + .collect { event -> + when (event) { + is StatusChangedEvent -> handleStatusChangedEvent(event.status) + is BlockEvent -> removeAllByAccountId(event.accountId) + is StatusComposedEvent -> handleStatusComposedEvent(event) + is StatusDeletedEvent -> handleStatusDeletedEvent(event) + } + } + } + + loadFilters() + } + + fun loadThread(id: String) { + _uiState.value = ThreadUiState.Loading + + viewModelScope.launch { + Log.d(TAG, "Finding status with: $id") + val contextCall = async { api.statusContext(id) } + val statusAndAccount = db.timelineStatusDao().getStatusWithAccount(accountManager.activeAccount!!.id, id) + + var detailedStatus = if (statusAndAccount != null) { + Log.d(TAG, "Loaded status from local timeline") + StatusViewData.Concrete( + status = statusAndAccount.first.toStatus(statusAndAccount.second), + isExpanded = statusAndAccount.first.expanded, + isShowingContent = statusAndAccount.first.contentShowing, + isCollapsed = statusAndAccount.first.contentCollapsed, + isDetailed = true, + translation = null + ) + } else { + Log.d(TAG, "Loaded status from network") + val result = api.status(id).getOrElse { exception -> + _uiState.value = ThreadUiState.Error(exception) + return@launch + } + result.toViewData(isDetailed = true) + } + + _uiState.value = ThreadUiState.LoadingThread( + statusViewDatum = detailedStatus, + revealButton = detailedStatus.getRevealButtonState() + ) + + // If the detailedStatus was loaded from the database it might be out-of-date + // compared to the remote one. Now the user has a working UI do a background fetch + // for the status. Ignore errors, the user still has a functioning UI if the fetch + // failed. Update the database when the fetch was successful. + if (statusAndAccount != null) { + api.status(id).onSuccess { result -> + db.timelineStatusDao().update( + tuskyAccountId = accountManager.activeAccount!!.id, + status = result, + moshi = moshi + ) + detailedStatus = result.toViewData(isDetailed = true) + } + } + + val contextResult = contextCall.await() + + contextResult.fold({ statusContext -> + val ancestors = + statusContext.ancestors.map { status -> status.toViewData() }.filter() + val descendants = + statusContext.descendants.map { status -> status.toViewData() }.filter() + val statuses = ancestors + detailedStatus + descendants + + _uiState.value = ThreadUiState.Success( + statusViewData = statuses, + detailedStatusPosition = ancestors.size, + revealButton = statuses.getRevealButtonState() + ) + }, { throwable -> + _errors.emit(throwable) + _uiState.value = ThreadUiState.Success( + statusViewData = listOf(detailedStatus), + detailedStatusPosition = 0, + revealButton = RevealButtonState.NO_BUTTON + ) + }) + } + } + + fun retry(id: String) { + _uiState.value = ThreadUiState.Loading + loadThread(id) + } + + fun refresh(id: String) { + _uiState.value = ThreadUiState.Refreshing + loadThread(id) + } + + fun detailedStatus(): StatusViewData.Concrete? { + return when (val uiState = _uiState.value) { + is ThreadUiState.Success -> uiState.statusViewData.find { status -> + status.isDetailed + } + + is ThreadUiState.LoadingThread -> uiState.statusViewDatum + else -> null + } + } + + fun reblog(reblog: Boolean, status: StatusViewData.Concrete): Job = viewModelScope.launch { + try { + timelineCases.reblog(status.actionableId, reblog).getOrThrow() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to reblog status " + status.actionableId, t) + } + } + } + + fun favorite(favorite: Boolean, status: StatusViewData.Concrete): Job = viewModelScope.launch { + try { + timelineCases.favourite(status.actionableId, favorite).getOrThrow() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to favourite status " + status.actionableId, t) + } + } + } + + fun bookmark(bookmark: Boolean, status: StatusViewData.Concrete): Job = viewModelScope.launch { + try { + timelineCases.bookmark(status.actionableId, bookmark).getOrThrow() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to bookmark status " + status.actionableId, t) + } + } + } + + fun voteInPoll(choices: List<Int>, status: StatusViewData.Concrete): Job = + viewModelScope.launch { + val poll = status.status.actionableStatus.poll ?: run { + Log.w(TAG, "No poll on status ${status.id}") + return@launch + } + + val votedPoll = poll.votedCopy(choices) + updateStatus(status.id) { status -> + status.copy(poll = votedPoll) + } + + try { + timelineCases.voteInPoll(status.actionableId, poll.id, choices).getOrThrow() + } catch (t: Exception) { + ifExpected(t) { + Log.d(TAG, "Failed to vote in poll: " + status.actionableId, t) + } + } + } + + fun removeStatus(statusToRemove: StatusViewData.Concrete) { + updateSuccess { uiState -> + uiState.copy( + statusViewData = uiState.statusViewData.filterNot { status -> status == statusToRemove } + ) + } + } + + fun changeExpanded(expanded: Boolean, status: StatusViewData.Concrete) { + updateSuccess { uiState -> + val statuses = uiState.statusViewData.map { viewData -> + if (viewData.id == status.id) { + viewData.copy(isExpanded = expanded) + } else { + viewData + } + } + uiState.copy( + statusViewData = statuses, + revealButton = statuses.getRevealButtonState() + ) + } + } + + fun changeContentShowing(isShowing: Boolean, status: StatusViewData.Concrete) { + updateStatusViewData(status.id) { viewData -> + viewData.copy(isShowingContent = isShowing) + } + } + + fun changeContentCollapsed(isCollapsed: Boolean, status: StatusViewData.Concrete) { + updateStatusViewData(status.id) { viewData -> + viewData.copy(isCollapsed = isCollapsed) + } + } + + suspend fun translate(status: StatusViewData.Concrete): NetworkResult<Unit> { + updateStatusViewData(status.id) { viewData -> + viewData.copy(translation = TranslationViewData.Loading) + } + return timelineCases.translate(status.actionableId) + .map { translation -> + updateStatusViewData(status.id) { viewData -> + viewData.copy(translation = TranslationViewData.Loaded(translation)) + } + } + .onFailure { + updateStatusViewData(status.id) { viewData -> + viewData.copy(translation = null) + } + } + } + + fun untranslate(status: StatusViewData.Concrete) { + updateStatusViewData(status.id) { viewData -> + viewData.copy(translation = null) + } + } + + private fun handleStatusChangedEvent(status: Status) { + updateStatusViewData(status.id) { viewData -> + status.toViewData( + isShowingContent = viewData.isShowingContent, + isExpanded = viewData.isExpanded, + isCollapsed = viewData.isCollapsed, + isDetailed = viewData.isDetailed, + translation = viewData.translation, + ) + } + } + + private fun removeAllByAccountId(accountId: String) { + updateSuccess { uiState -> + uiState.copy( + statusViewData = uiState.statusViewData.filter { viewData -> + viewData.status.account.id != accountId + } + ) + } + } + + private fun handleStatusComposedEvent(event: StatusComposedEvent) { + val eventStatus = event.status + updateSuccess { uiState -> + val statuses = uiState.statusViewData + val detailedIndex = statuses.indexOfFirst { status -> status.isDetailed } + val repliedIndex = + statuses.indexOfFirst { status -> eventStatus.inReplyToId == status.id } + if (detailedIndex != -1 && repliedIndex >= detailedIndex) { + // there is a new reply to the detailed status or below -> display it + val newStatuses = statuses.subList(0, repliedIndex + 1) + + eventStatus.toViewData() + + statuses.subList(repliedIndex + 1, statuses.size) + uiState.copy(statusViewData = newStatuses) + } else { + uiState + } + } + } + + private fun handleStatusDeletedEvent(event: StatusDeletedEvent) { + updateSuccess { uiState -> + uiState.copy( + statusViewData = uiState.statusViewData.filter { status -> + status.id != event.statusId + } + ) + } + } + + fun toggleRevealButton() { + updateSuccess { uiState -> + when (uiState.revealButton) { + RevealButtonState.HIDE -> uiState.copy( + statusViewData = uiState.statusViewData.map { viewData -> + viewData.copy(isExpanded = false) + }, + revealButton = RevealButtonState.REVEAL + ) + + RevealButtonState.REVEAL -> uiState.copy( + statusViewData = uiState.statusViewData.map { viewData -> + viewData.copy(isExpanded = true) + }, + revealButton = RevealButtonState.HIDE + ) + + else -> uiState + } + } + } + + private fun StatusViewData.Concrete.getRevealButtonState(): RevealButtonState { + val hasWarnings = status.spoilerText.isNotEmpty() + + return if (hasWarnings) { + if (isExpanded) { + RevealButtonState.HIDE + } else { + RevealButtonState.REVEAL + } + } else { + RevealButtonState.NO_BUTTON + } + } + + /** + * Get the reveal button state based on the state of all the statuses in the list. + * + * - If any status sets it to REVEAL, use REVEAL + * - If no status sets it to REVEAL, but at least one uses HIDE, use HIDE + * - Otherwise use NO_BUTTON + */ + private fun List<StatusViewData.Concrete>.getRevealButtonState(): RevealButtonState { + var seenHide = false + + forEach { + when (val state = it.getRevealButtonState()) { + RevealButtonState.NO_BUTTON -> return@forEach + RevealButtonState.REVEAL -> return state + RevealButtonState.HIDE -> seenHide = true + } + } + + if (seenHide) { + return RevealButtonState.HIDE + } + + return RevealButtonState.NO_BUTTON + } + + private fun loadFilters() { + viewModelScope.launch { + api.getFilters().fold( + { + filterModel.kind = Filter.Kind.THREAD + updateStatuses() + }, + { throwable -> + if (throwable.isHttpNotFound()) { + val filters = api.getFiltersV1().getOrElse { + Log.w(TAG, "Failed to fetch filters", it) + return@launch + } + + filterModel.initWithFilters( + filters.filter { filter -> filter.context.contains(FilterV1.THREAD) } + ) + updateStatuses() + } else { + Log.e(TAG, "Error getting filters", throwable) + } + } + ) + } + } + + private fun updateStatuses() { + updateSuccess { uiState -> + val statuses = uiState.statusViewData.filter() + uiState.copy( + statusViewData = statuses, + revealButton = statuses.getRevealButtonState() + ) + } + } + + private fun List<StatusViewData.Concrete>.filter(): List<StatusViewData.Concrete> { + return filter { status -> + if (status.isDetailed) { + true + } else { + status.filterAction = filterModel.shouldFilterStatus(status.status) + status.filterAction != Filter.Action.HIDE + } + } + } + + private fun Status.toViewData(isDetailed: Boolean = false): StatusViewData.Concrete { + val oldStatus = (_uiState.value as? ThreadUiState.Success)?.statusViewData?.find { + it.id == this.id + } + return toViewData( + isShowingContent = oldStatus?.isShowingContent + ?: (alwaysShowSensitiveMedia || !actionableStatus.sensitive), + isExpanded = oldStatus?.isExpanded ?: alwaysOpenSpoiler, + isCollapsed = oldStatus?.isCollapsed ?: !isDetailed, + isDetailed = oldStatus?.isDetailed ?: isDetailed + ) + } + + private inline fun updateSuccess(updater: (ThreadUiState.Success) -> ThreadUiState.Success) { + _uiState.update { uiState -> + if (uiState is ThreadUiState.Success) { + updater(uiState) + } else { + uiState + } + } + } + + private fun updateStatusViewData( + statusId: String, + updater: (StatusViewData.Concrete) -> StatusViewData.Concrete + ) { + updateSuccess { uiState -> + uiState.copy( + statusViewData = uiState.statusViewData.map { viewData -> + if (viewData.id == statusId) { + updater(viewData) + } else { + viewData + } + } + ) + } + } + + private fun updateStatus(statusId: String, updater: (Status) -> Status) { + updateStatusViewData(statusId) { viewData -> + viewData.copy( + status = updater(viewData.status) + ) + } + } + + fun clearWarning(viewData: StatusViewData.Concrete) { + updateStatus(viewData.id) { status -> + status.copy(filtered = emptyList()) + } + } + + companion object { + private const val TAG = "ViewThreadViewModel" + } +} + +sealed interface ThreadUiState { + /** The initial load of the detailed status for this thread */ + data object Loading : ThreadUiState + + /** Loading the detailed status has completed, now loading ancestors/descendants */ + data class LoadingThread( + val statusViewDatum: StatusViewData.Concrete?, + val revealButton: RevealButtonState + ) : ThreadUiState + + /** An error occurred at any point */ + class Error(val throwable: Throwable) : ThreadUiState + + /** Successfully loaded the full thread */ + data class Success( + val statusViewData: List<StatusViewData.Concrete>, + val revealButton: RevealButtonState, + val detailedStatusPosition: Int + ) : ThreadUiState + + /** Refreshing the thread with a swipe */ + data object Refreshing : ThreadUiState +} + +enum class RevealButtonState { + NO_BUTTON, + REVEAL, + HIDE +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsAdapter.kt new file mode 100644 index 0000000..006d90b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsAdapter.kt @@ -0,0 +1,311 @@ +package com.keylesspalace.tusky.components.viewthread.edits + +import android.content.Context +import android.graphics.Typeface.DEFAULT_BOLD +import android.graphics.drawable.ColorDrawable +import android.graphics.drawable.Drawable +import android.text.Editable +import android.text.SpannableStringBuilder +import android.text.TextPaint +import android.text.style.CharacterStyle +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.bumptech.glide.Glide +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.PollAdapter +import com.keylesspalace.tusky.adapter.PollAdapter.Companion.MULTIPLE +import com.keylesspalace.tusky.adapter.PollAdapter.Companion.SINGLE +import com.keylesspalace.tusky.databinding.ItemStatusEditBinding +import com.keylesspalace.tusky.entity.Attachment.Focus +import com.keylesspalace.tusky.entity.StatusEdit +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.util.AbsoluteTimeFormatter +import com.keylesspalace.tusky.util.BindingHolder +import com.keylesspalace.tusky.util.TuskyTagHandler +import com.keylesspalace.tusky.util.aspectRatios +import com.keylesspalace.tusky.util.decodeBlurHash +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.parseAsMastodonHtml +import com.keylesspalace.tusky.util.setClickableText +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.viewdata.toViewData +import org.xml.sax.XMLReader + +class ViewEditsAdapter( + private val edits: List<StatusEdit>, + private val animateEmojis: Boolean, + private val useBlurhash: Boolean, + private val listener: LinkListener +) : RecyclerView.Adapter<BindingHolder<ItemStatusEditBinding>>() { + + private val absoluteTimeFormatter = AbsoluteTimeFormatter() + + /** Size of large text in this theme, in px */ + private var largeTextSizePx: Float = 0f + + /** Size of medium text in this theme, in px */ + private var mediumTextSizePx: Float = 0f + + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int + ): BindingHolder<ItemStatusEditBinding> { + val binding = ItemStatusEditBinding.inflate( + LayoutInflater.from(parent.context), + parent, + false + ) + + binding.statusEditMediaPreview.clipToOutline = true + + val typedValue = TypedValue() + val context = binding.root.context + val displayMetrics = context.resources.displayMetrics + context.theme.resolveAttribute(R.attr.status_text_large, typedValue, true) + largeTextSizePx = typedValue.getDimension(displayMetrics) + context.theme.resolveAttribute(R.attr.status_text_medium, typedValue, true) + mediumTextSizePx = typedValue.getDimension(displayMetrics) + + return BindingHolder(binding) + } + + override fun onBindViewHolder(holder: BindingHolder<ItemStatusEditBinding>, position: Int) { + val edit = edits[position] + + val binding = holder.binding + + val context = binding.root.context + + val infoStringRes = if (position == edits.lastIndex) { + R.string.status_created_info + } else { + R.string.status_edit_info + } + + // Show the most recent version of the status using large text to make it clearer for + // the user, and for similarity with thread view. + val variableTextSize = if (position == edits.lastIndex) { + mediumTextSizePx + } else { + largeTextSizePx + } + binding.statusEditContentWarningDescription.setTextSize( + TypedValue.COMPLEX_UNIT_PX, + variableTextSize + ) + binding.statusEditContent.setTextSize(TypedValue.COMPLEX_UNIT_PX, variableTextSize) + binding.statusEditMediaSensitivity.setTextSize(TypedValue.COMPLEX_UNIT_PX, variableTextSize) + + val timestamp = absoluteTimeFormatter.format(edit.createdAt, false) + + binding.statusEditInfo.text = context.getString(infoStringRes, timestamp) + + if (edit.spoilerText.isEmpty()) { + binding.statusEditContentWarningDescription.hide() + binding.statusEditContentWarningSeparator.hide() + } else { + binding.statusEditContentWarningDescription.show() + binding.statusEditContentWarningSeparator.show() + binding.statusEditContentWarningDescription.text = edit.spoilerText.emojify( + edit.emojis, + binding.statusEditContentWarningDescription, + animateEmojis + ) + } + + val emojifiedText = edit + .content + .parseAsMastodonHtml(EditsTagHandler(context)) + .emojify(edit.emojis, binding.statusEditContent, animateEmojis) + + setClickableText( + binding.statusEditContent, + emojifiedText, + emptyList(), + emptyList(), + listener + ) + + if (edit.poll == null) { + binding.statusEditPollOptions.hide() + binding.statusEditPollDescription.hide() + } else { + binding.statusEditPollOptions.show() + + // not used for now since not reported by the api + // https://github.com/mastodon/mastodon/issues/22571 + // binding.statusEditPollDescription.show() + + val pollAdapter = PollAdapter() + binding.statusEditPollOptions.adapter = pollAdapter + binding.statusEditPollOptions.layoutManager = LinearLayoutManager(context) + + pollAdapter.setup( + options = edit.poll.options.map { it.toViewData(false) }, + voteCount = 0, + votersCount = null, + emojis = edit.emojis, + mode = if (edit.poll.multiple) { // not reported by the api + MULTIPLE + } else { + SINGLE + }, + resultClickListener = null, + animateEmojis = animateEmojis, + enabled = false + ) + } + + if (edit.mediaAttachments.isEmpty()) { + binding.statusEditMediaPreview.hide() + binding.statusEditMediaSensitivity.hide() + } else { + binding.statusEditMediaPreview.show() + binding.statusEditMediaPreview.aspectRatios = edit.mediaAttachments.aspectRatios() + + binding.statusEditMediaPreview.forEachIndexed { index, imageView, descriptionIndicator -> + + val attachment = edit.mediaAttachments[index] + val hasDescription = !attachment.description.isNullOrBlank() + + if (hasDescription) { + imageView.contentDescription = attachment.description + } else { + imageView.contentDescription = + imageView.context.getString(R.string.action_view_media) + } + descriptionIndicator.visibility = if (hasDescription) View.VISIBLE else View.GONE + + val blurhash = attachment.blurhash + + val placeholder: Drawable = if (blurhash != null && useBlurhash) { + decodeBlurHash(context, blurhash) + } else { + ColorDrawable(MaterialColors.getColor(imageView, R.attr.colorBackgroundAccent)) + } + + if (attachment.previewUrl.isNullOrEmpty()) { + imageView.removeFocalPoint() + Glide.with(imageView) + .load(placeholder) + .centerInside() + .into(imageView) + } else { + val focus: Focus? = attachment.meta?.focus + + if (focus != null) { + imageView.setFocalPoint(focus) + Glide.with(imageView.context) + .load(attachment.previewUrl) + .placeholder(placeholder) + .centerInside() + .addListener(imageView) + .into(imageView) + } else { + imageView.removeFocalPoint() + Glide.with(imageView) + .load(attachment.previewUrl) + .placeholder(placeholder) + .centerInside() + .into(imageView) + } + } + } + binding.statusEditMediaSensitivity.visible(edit.sensitive) + } + } + + override fun getItemCount() = edits.size + + companion object { + private const val VIEW_TYPE_EDITS_NEWEST = 0 + private const val VIEW_TYPE_EDITS = 1 + } +} + +/** + * Handle XML tags created by [ViewEditsViewModel] and create custom spans to display inserted or + * deleted text. + */ +class EditsTagHandler(val context: Context) : TuskyTagHandler() { + /** Class to mark the start of a span of deleted text */ + class Del + + /** Class to mark the start of a span of inserted text */ + class Ins + + override fun handleTag(opening: Boolean, tag: String, output: Editable, xmlReader: XMLReader) { + when (tag) { + DELETED_TEXT_EL -> { + if (opening) { + start(output as SpannableStringBuilder, Del()) + } else { + end( + output as SpannableStringBuilder, + Del::class.java, + DeletedTextSpan(context) + ) + } + } + INSERTED_TEXT_EL -> { + if (opening) { + start(output as SpannableStringBuilder, Ins()) + } else { + end( + output as SpannableStringBuilder, + Ins::class.java, + InsertedTextSpan(context) + ) + } + } + else -> super.handleTag(opening, tag, output, xmlReader) + } + } + + /** Span that signifies deleted text */ + class DeletedTextSpan(context: Context) : CharacterStyle() { + private var bgColor: Int + + init { + bgColor = context.getColor(R.color.view_edits_background_delete) + } + + override fun updateDrawState(tp: TextPaint) { + tp.bgColor = bgColor + tp.isStrikeThruText = true + } + } + + /** Span that signifies inserted text */ + class InsertedTextSpan(context: Context) : CharacterStyle() { + private var bgColor: Int + + init { + bgColor = context.getColor(R.color.view_edits_background_insert) + } + + override fun updateDrawState(tp: TextPaint) { + tp.bgColor = bgColor + tp.typeface = DEFAULT_BOLD + } + } + + companion object { + /** XML element to represent text that has been deleted */ + // Can't be an element that Android's HTML parser recognises, otherwise the tagHandler + // won't be called for it. + const val DELETED_TEXT_EL = "tusky-del" + + /** XML element to represent text that has been inserted */ + // Can't be an element that Android's HTML parser recognises, otherwise the tagHandler + // won't be called for it. + const val INSERTED_TEXT_EL = "tusky-ins" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt new file mode 100644 index 0000000..10301ca --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsFragment.kt @@ -0,0 +1,225 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.viewthread.edits + +import android.content.SharedPreferences +import android.os.Bundle +import android.util.Log +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.widget.LinearLayout +import androidx.core.view.MenuProvider +import androidx.fragment.app.Fragment +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.SimpleItemAnimator +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.StatusListActivity +import com.keylesspalace.tusky.components.account.AccountActivity +import com.keylesspalace.tusky.databinding.FragmentViewEditsBinding +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.settings.PrefKeys +import com.keylesspalace.tusky.util.emojify +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.loadAvatar +import com.keylesspalace.tusky.util.show +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import com.keylesspalace.tusky.util.unicodeWrap +import com.keylesspalace.tusky.util.viewBinding +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizeDp +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class ViewEditsFragment : + Fragment(R.layout.fragment_view_edits), + LinkListener, + OnRefreshListener, + MenuProvider { + + @Inject + lateinit var preferences: SharedPreferences + + private val viewModel: ViewEditsViewModel by viewModels() + + private val binding by viewBinding(FragmentViewEditsBinding::bind) + + private lateinit var statusId: String + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + requireActivity().addMenuProvider(this, viewLifecycleOwner, Lifecycle.State.RESUMED) + + binding.swipeRefreshLayout.setOnRefreshListener(this) + + binding.recyclerView.setHasFixedSize(true) + binding.recyclerView.layoutManager = LinearLayoutManager(context) + + val divider = DividerItemDecoration(context, LinearLayout.VERTICAL) + binding.recyclerView.addItemDecoration(divider) + (binding.recyclerView.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false + + statusId = requireArguments().getString(STATUS_ID_EXTRA)!! + + val animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false) + val animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false) + val useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true) + val avatarRadius: Int = requireContext().resources.getDimensionPixelSize( + R.dimen.avatar_radius_48dp + ) + + viewLifecycleOwner.lifecycleScope.launch { + viewModel.uiState.collect { uiState -> + when (uiState) { + EditsUiState.Initial -> {} + EditsUiState.Loading -> { + binding.recyclerView.hide() + binding.statusView.hide() + binding.initialProgressBar.show() + } + EditsUiState.Refreshing -> {} + is EditsUiState.Error -> { + Log.w(TAG, "failed to load edits", uiState.throwable) + + binding.swipeRefreshLayout.isRefreshing = false + binding.recyclerView.hide() + binding.statusView.show() + binding.initialProgressBar.hide() + + when (uiState.throwable) { + is ViewEditsViewModel.MissingEditsException -> { + binding.statusView.setup( + R.drawable.elephant_friend_empty, + R.string.error_missing_edits + ) + } + else -> { + binding.statusView.setup(uiState.throwable) { + viewModel.loadEdits(statusId, force = true) + } + } + } + } + is EditsUiState.Success -> { + binding.swipeRefreshLayout.isRefreshing = false + binding.recyclerView.show() + binding.statusView.hide() + binding.initialProgressBar.hide() + + binding.recyclerView.adapter = ViewEditsAdapter( + edits = uiState.edits, + animateEmojis = animateEmojis, + useBlurhash = useBlurhash, + listener = this@ViewEditsFragment + ) + + // Focus on the most recent version + (binding.recyclerView.layoutManager as LinearLayoutManager).scrollToPosition( + 0 + ) + + val account = uiState.edits.first().account + loadAvatar( + account.avatar, + binding.statusAvatar, + avatarRadius, + animateAvatars + ) + + binding.statusDisplayName.text = account.name.unicodeWrap().emojify(account.emojis, binding.statusDisplayName, animateEmojis) + binding.statusUsername.text = account.username + } + } + } + } + + viewModel.loadEdits(statusId) + } + + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.fragment_view_edits, menu) + menu.findItem(R.id.action_refresh)?.apply { + icon = IconicsDrawable(requireContext(), GoogleMaterial.Icon.gmd_refresh).apply { + sizeDp = 20 + colorInt = MaterialColors.getColor(binding.root, android.R.attr.textColorPrimary) + } + } + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + return when (menuItem.itemId) { + R.id.action_refresh -> { + binding.swipeRefreshLayout.isRefreshing = true + onRefresh() + true + } + else -> false + } + } + + override fun onResume() { + super.onResume() + requireActivity().title = getString(R.string.title_edits) + } + + override fun onRefresh() { + viewModel.loadEdits(statusId, force = true, refreshing = true) + } + + override fun onViewAccount(id: String) { + bottomSheetActivity?.startActivityWithSlideInAnimation( + AccountActivity.getIntent(requireContext(), id) + ) + } + + override fun onViewTag(tag: String) { + bottomSheetActivity?.startActivityWithSlideInAnimation( + StatusListActivity.newHashtagIntent(requireContext(), tag) + ) + } + + override fun onViewUrl(url: String) { + bottomSheetActivity?.viewUrl(url) + } + + private val bottomSheetActivity + get() = (activity as? BottomSheetActivity) + + companion object { + private const val TAG = "ViewEditsFragment" + + private const val STATUS_ID_EXTRA = "id" + + fun newInstance(statusId: String): ViewEditsFragment { + val arguments = Bundle(1) + val fragment = ViewEditsFragment() + arguments.putString(STATUS_ID_EXTRA, statusId) + fragment.arguments = arguments + return fragment + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt new file mode 100644 index 0000000..96b7ecd --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/components/viewthread/edits/ViewEditsViewModel.kt @@ -0,0 +1,211 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.viewthread.edits + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.getOrElse +import com.keylesspalace.tusky.components.viewthread.edits.EditsTagHandler.Companion.DELETED_TEXT_EL +import com.keylesspalace.tusky.components.viewthread.edits.EditsTagHandler.Companion.INSERTED_TEXT_EL +import com.keylesspalace.tusky.entity.StatusEdit +import com.keylesspalace.tusky.network.MastodonApi +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch +import org.pageseeder.diffx.api.LoadingException +import org.pageseeder.diffx.api.Operator +import org.pageseeder.diffx.config.DiffConfig +import org.pageseeder.diffx.config.TextGranularity +import org.pageseeder.diffx.config.WhiteSpaceProcessing +import org.pageseeder.diffx.core.OptimisticXMLProcessor +import org.pageseeder.diffx.format.XMLDiffOutput +import org.pageseeder.diffx.load.SAXLoader +import org.pageseeder.diffx.token.XMLToken +import org.pageseeder.diffx.token.XMLTokenType +import org.pageseeder.diffx.token.impl.SpaceToken +import org.pageseeder.diffx.xml.NamespaceSet +import org.pageseeder.xmlwriter.XML.NamespaceAware +import org.pageseeder.xmlwriter.XMLStringWriter + +@HiltViewModel +class ViewEditsViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() { + + private val _uiState = MutableStateFlow(EditsUiState.Initial as EditsUiState) + val uiState: StateFlow<EditsUiState> = _uiState.asStateFlow() + + /** The API call to fetch edit history returned less than two items */ + class MissingEditsException : Exception() + + fun loadEdits(statusId: String, force: Boolean = false, refreshing: Boolean = false) { + if (!force && _uiState.value !is EditsUiState.Initial) return + + if (refreshing) { + _uiState.value = EditsUiState.Refreshing + } else { + _uiState.value = EditsUiState.Loading + } + + viewModelScope.launch { + val edits = api.statusEdits(statusId).getOrElse { + _uiState.value = EditsUiState.Error(it) + return@launch + } + + // `edits` might have fewer than the minimum number of entries because of + // https://github.com/mastodon/mastodon/issues/25398. + if (edits.size < 2) { + _uiState.value = EditsUiState.Error(MissingEditsException()) + return@launch + } + + // Diff each status' content against the previous version, producing new + // content with additional `ins` or `del` elements marking inserted or + // deleted content. + // + // This can be CPU intensive depending on the number of edits and the size + // of each, so don't run this on Dispatchers.Main. + viewModelScope.launch(Dispatchers.Default) { + val sortedEdits = edits.sortedBy { it.createdAt } + .reversed() + .toMutableList() + + SAXLoader.setXMLReaderClass("org.xmlpull.v1.sax2.Driver") + val loader = SAXLoader() + loader.config = DiffConfig( + false, + WhiteSpaceProcessing.PRESERVE, + TextGranularity.SPACE_WORD + ) + val processor = OptimisticXMLProcessor() + processor.setCoalesce(true) + val output = HtmlDiffOutput() + + try { + // The XML processor expects `br` to be closed + var currentContent = + loader.load(sortedEdits[0].content.replace("<br>", "<br/>")) + var previousContent = + loader.load(sortedEdits[1].content.replace("<br>", "<br/>")) + + for (i in 1 until sortedEdits.size) { + processor.diff(previousContent, currentContent, output) + sortedEdits[i - 1] = sortedEdits[i - 1].copy( + content = output.xml.toString() + ) + + if (i < sortedEdits.size - 1) { + currentContent = previousContent + previousContent = loader.load( + sortedEdits[i + 1].content.replace("<br>", "<br/>") + ) + } + } + _uiState.value = EditsUiState.Success(sortedEdits) + } catch (_: LoadingException) { + // Something failed parsing the XML from the server. Rather than + // show an error just return the sorted edits so the user can at + // least visually scan the differences. + _uiState.value = EditsUiState.Success(sortedEdits) + } + } + } + } + + companion object { + const val TAG = "ViewEditsViewModel" + } +} + +sealed interface EditsUiState { + data object Initial : EditsUiState + data object Loading : EditsUiState + + // "Refreshing" state is necessary, otherwise a refresh state transition is Success -> Success, + // and state flows don't emit repeated states, so the UI never updates. + data object Refreshing : EditsUiState + class Error(val throwable: Throwable) : EditsUiState + data class Success( + val edits: List<StatusEdit> + ) : EditsUiState +} + +/** + * Add elements wrapping inserted or deleted content. + */ +class HtmlDiffOutput : XMLDiffOutput { + /** XML Output */ + lateinit var xml: XMLStringWriter + private set + + override fun start() { + xml = XMLStringWriter(NamespaceAware.Yes) + } + + override fun handle(operator: Operator, token: XMLToken) { + if (operator.isEdit) { + handleEdit(operator, token) + } else { + token.toXML(xml) + } + } + + override fun end() { + xml.flush() + } + + override fun setWriteXMLDeclaration(show: Boolean) { + // This space intentionally left blank + } + + override fun setNamespaces(namespaces: NamespaceSet?) { + // This space intentionally left blank + } + + private fun handleEdit(operator: Operator, token: XMLToken) { + if (token == SpaceToken.NEW_LINE) { + if (operator == Operator.INS) { + token.toXML(xml) + } + return + } + when (token.type) { + XMLTokenType.START_ELEMENT -> token.toXML(xml) + XMLTokenType.END_ELEMENT -> token.toXML(xml) + XMLTokenType.TEXT -> { + // wrap the characters in a <tusky-ins/tusky-del> element + when (operator) { + Operator.DEL -> DELETED_TEXT_EL + Operator.INS -> INSERTED_TEXT_EL + else -> null + }?.let { + xml.openElement(it, false) + } + token.toXML(xml) + xml.closeElement() + } + else -> { + // Only include inserted content + if (operator === Operator.INS) { + token.toXML(xml) + } + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt new file mode 100644 index 0000000..32e50f7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/AccountManager.kt @@ -0,0 +1,255 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db + +import android.content.SharedPreferences +import android.util.Log +import com.keylesspalace.tusky.db.dao.AccountDao +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.settings.PrefKeys +import java.util.Locale +import javax.inject.Inject +import javax.inject.Singleton + +/** + * This class caches the account database and handles all account related operations + * @author ConnyDuck + */ + +private const val TAG = "AccountManager" + +@Singleton +class AccountManager @Inject constructor( + db: AppDatabase, + private val preferences: SharedPreferences +) { + + @Volatile + var activeAccount: AccountEntity? = null + private set + + var accounts: MutableList<AccountEntity> = mutableListOf() + private set + + private val accountDao: AccountDao = db.accountDao() + + init { + accounts = accountDao.loadAll().toMutableList() + + activeAccount = accounts.find { acc -> acc.isActive } + ?: accounts.firstOrNull()?.also { acc -> acc.isActive = true } + } + + /** + * Adds a new account and makes it the active account. + * @param accessToken the access token for the new account + * @param domain the domain of the account's Mastodon instance + * @param clientId the oauth client id used to sign in the account + * @param clientSecret the oauth client secret used to sign in the account + * @param oauthScopes the oauth scopes granted to the account + * @param newAccount the [Account] as returned by the Mastodon Api + */ + fun addAccount( + accessToken: String, + domain: String, + clientId: String, + clientSecret: String, + oauthScopes: String, + newAccount: Account + ) { + activeAccount?.let { + it.isActive = false + Log.d(TAG, "addAccount: saving account with id " + it.id) + + accountDao.insertOrReplace(it) + } + // check if this is a relogin with an existing account, if yes update it, otherwise create a new one + val existingAccountIndex = accounts.indexOfFirst { account -> + domain == account.domain && newAccount.id == account.accountId + } + val newAccountEntity = if (existingAccountIndex != -1) { + accounts[existingAccountIndex].copy( + accessToken = accessToken, + clientId = clientId, + clientSecret = clientSecret, + oauthScopes = oauthScopes, + isActive = true + ).also { accounts[existingAccountIndex] = it } + } else { + val maxAccountId = accounts.maxByOrNull { it.id }?.id ?: 0 + val newAccountId = maxAccountId + 1 + AccountEntity( + id = newAccountId, + domain = domain.lowercase(Locale.ROOT), + accessToken = accessToken, + clientId = clientId, + clientSecret = clientSecret, + oauthScopes = oauthScopes, + isActive = true, + accountId = newAccount.id + ).also { accounts.add(it) } + } + + activeAccount = newAccountEntity + updateActiveAccount(newAccount) + } + + /** + * Saves an already known account to the database. + * New accounts must be created with [addAccount] + * @param account the account to save + */ + fun saveAccount(account: AccountEntity) { + if (account.id != 0L) { + Log.d(TAG, "saveAccount: saving account with id " + account.id) + accountDao.insertOrReplace(account) + } + } + + /** + * Logs the current account out by deleting all data of the account. + * @return the new active account, or null if no other account was found + */ + fun logActiveAccountOut(): AccountEntity? { + return activeAccount?.let { account -> + + account.logout() + + accounts.remove(account) + accountDao.delete(account) + + if (accounts.size > 0) { + accounts[0].isActive = true + activeAccount = accounts[0] + Log.d(TAG, "logActiveAccountOut: saving account with id " + accounts[0].id) + accountDao.insertOrReplace(accounts[0]) + } else { + activeAccount = null + } + activeAccount + } + } + + /** + * updates the current account with new information from the mastodon api + * and saves it in the database + * @param account the [Account] object returned from the api + */ + fun updateActiveAccount(account: Account) { + activeAccount?.let { + it.accountId = account.id + it.username = account.username + it.displayName = account.name + it.profilePictureUrl = account.avatar + it.defaultPostPrivacy = account.source?.privacy ?: Status.Visibility.PUBLIC + it.defaultPostLanguage = account.source?.language.orEmpty() + it.defaultMediaSensitivity = account.source?.sensitive ?: false + it.emojis = account.emojis + it.locked = account.locked + + Log.d(TAG, "updateActiveAccount: saving account with id " + it.id) + accountDao.insertOrReplace(it) + } + } + + /** + * changes the active account + * @param accountId the database id of the new active account + */ + fun setActiveAccount(accountId: Long) { + val newActiveAccount = accounts.find { (id) -> + id == accountId + } ?: return // invalid accountId passed, do nothing + + activeAccount?.let { + Log.d(TAG, "setActiveAccount: saving account with id " + it.id) + it.isActive = false + saveAccount(it) + } + + activeAccount = newActiveAccount + + activeAccount?.let { + it.isActive = true + accountDao.insertOrReplace(it) + } + } + + /** + * @return an immutable list of all accounts in the database with the active account first + */ + fun getAllAccountsOrderedByActive(): List<AccountEntity> { + val accountsCopy = accounts.toMutableList() + accountsCopy.sortWith { l, r -> + when { + l.isActive && !r.isActive -> -1 + r.isActive && !l.isActive -> 1 + else -> 0 + } + } + + return accountsCopy + } + + /** + * @return true if at least one account has notifications enabled + */ + fun areNotificationsEnabled(): Boolean { + return accounts.any { it.notificationsEnabled } + } + + /** + * Finds an account by its database id + * @param accountId the id of the account + * @return the requested account or null if it was not found + */ + fun getAccountById(accountId: Long): AccountEntity? { + return accounts.find { (id) -> + id == accountId + } + } + + /** + * Finds an account by its string identifier + * @param identifier the string identifier of the account + * @return the requested account or null if it was not found + */ + fun getAccountByIdentifier(identifier: String): AccountEntity? { + return accounts.find { + identifier == it.identifier + } + } + + /** + * @return true if the name of the currently-selected account should be displayed in UIs + */ + fun shouldDisplaySelfUsername(): Boolean { + val showUsernamePreference = preferences.getString( + PrefKeys.SHOW_SELF_USERNAME, + "disambiguate" + ) + if (showUsernamePreference == "always") { + return true + } + if (showUsernamePreference == "never") { + return false + } + + return accounts.size > 1 // "disambiguate" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java new file mode 100644 index 0000000..879e168 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/AppDatabase.java @@ -0,0 +1,851 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.room.AutoMigration; +import androidx.room.Database; +import androidx.room.DeleteColumn; +import androidx.room.RoomDatabase; +import androidx.room.migration.AutoMigrationSpec; +import androidx.room.migration.Migration; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import com.keylesspalace.tusky.TabDataKt; +import com.keylesspalace.tusky.components.conversation.ConversationEntity; +import com.keylesspalace.tusky.db.dao.AccountDao; +import com.keylesspalace.tusky.db.dao.DraftDao; +import com.keylesspalace.tusky.db.dao.InstanceDao; +import com.keylesspalace.tusky.db.dao.NotificationsDao; +import com.keylesspalace.tusky.db.dao.TimelineAccountDao; +import com.keylesspalace.tusky.db.dao.TimelineDao; +import com.keylesspalace.tusky.db.dao.TimelineStatusDao; +import com.keylesspalace.tusky.db.entity.AccountEntity; +import com.keylesspalace.tusky.db.entity.DraftEntity; +import com.keylesspalace.tusky.db.entity.HomeTimelineEntity; +import com.keylesspalace.tusky.db.entity.InstanceEntity; +import com.keylesspalace.tusky.db.entity.NotificationEntity; +import com.keylesspalace.tusky.db.entity.NotificationReportEntity; +import com.keylesspalace.tusky.db.entity.TimelineAccountEntity; +import com.keylesspalace.tusky.db.entity.TimelineStatusEntity; + +import java.io.File; + +/** + * DB version & declare DAO + */ +@Database( + entities = { + DraftEntity.class, + AccountEntity.class, + InstanceEntity.class, + TimelineStatusEntity.class, + TimelineAccountEntity.class, + ConversationEntity.class, + NotificationEntity.class, + NotificationReportEntity.class, + HomeTimelineEntity.class + }, + // Note: Starting with version 54, database versions in Tusky are always even. + // This is to reserve odd version numbers for use by forks. + version = 62, + autoMigrations = { + @AutoMigration(from = 48, to = 49), + @AutoMigration(from = 49, to = 50, spec = AppDatabase.MIGRATION_49_50.class), + @AutoMigration(from = 50, to = 51), + @AutoMigration(from = 51, to = 52), + @AutoMigration(from = 53, to = 54), // hasDirectMessageBadge in AccountEntity + @AutoMigration(from = 56, to = 58) // translationEnabled in InstanceEntity/InstanceInfoEntity + } +) +public abstract class AppDatabase extends RoomDatabase { + + @NonNull public abstract AccountDao accountDao(); + @NonNull public abstract InstanceDao instanceDao(); + @NonNull public abstract ConversationsDao conversationDao(); + @NonNull public abstract TimelineDao timelineDao(); + @NonNull public abstract DraftDao draftDao(); + @NonNull public abstract NotificationsDao notificationsDao(); + @NonNull public abstract TimelineStatusDao timelineStatusDao(); + @NonNull public abstract TimelineAccountDao timelineAccountDao(); + + public static final Migration MIGRATION_2_3 = new Migration(2, 3) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE TootEntity2 (uid INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, text TEXT, urls TEXT, contentWarning TEXT);"); + database.execSQL("INSERT INTO TootEntity2 SELECT * FROM TootEntity;"); + database.execSQL("DROP TABLE TootEntity;"); + database.execSQL("ALTER TABLE TootEntity2 RENAME TO TootEntity;"); + } + }; + + public static final Migration MIGRATION_3_4 = new Migration(3, 4) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToId TEXT"); + database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToText TEXT"); + database.execSQL("ALTER TABLE TootEntity ADD COLUMN inReplyToUsername TEXT"); + database.execSQL("ALTER TABLE TootEntity ADD COLUMN visibility INTEGER"); + } + }; + + public static final Migration MIGRATION_4_5 = new Migration(4, 5) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE `AccountEntity` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`domain` TEXT NOT NULL, `accessToken` TEXT NOT NULL, " + + "`isActive` INTEGER NOT NULL, `accountId` TEXT NOT NULL, " + + "`username` TEXT NOT NULL, `displayName` TEXT NOT NULL, " + + "`profilePictureUrl` TEXT NOT NULL, " + + "`notificationsEnabled` INTEGER NOT NULL, " + + "`notificationsMentioned` INTEGER NOT NULL, " + + "`notificationsFollowed` INTEGER NOT NULL, " + + "`notificationsReblogged` INTEGER NOT NULL, " + + "`notificationsFavorited` INTEGER NOT NULL, " + + "`notificationSound` INTEGER NOT NULL, " + + "`notificationVibration` INTEGER NOT NULL, " + + "`notificationLight` INTEGER NOT NULL, " + + "`lastNotificationId` TEXT NOT NULL, " + + "`activeNotifications` TEXT NOT NULL)"); + database.execSQL("CREATE UNIQUE INDEX `index_AccountEntity_domain_accountId` ON `AccountEntity` (`domain`, `accountId`)"); + } + }; + + public static final Migration MIGRATION_5_6 = new Migration(5, 6) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS `EmojiListEntity` (`instance` TEXT NOT NULL, `emojiList` TEXT NOT NULL, PRIMARY KEY(`instance`))"); + } + }; + + public static final Migration MIGRATION_6_7 = new Migration(6, 7) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS `InstanceEntity` (`instance` TEXT NOT NULL, `emojiList` TEXT, `maximumTootCharacters` INTEGER, PRIMARY KEY(`instance`))"); + database.execSQL("INSERT OR REPLACE INTO `InstanceEntity` SELECT `instance`,`emojiList`, NULL FROM `EmojiListEntity`;"); + database.execSQL("DROP TABLE `EmojiListEntity`;"); + } + }; + + public static final Migration MIGRATION_7_8 = new Migration(7, 8) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `emojis` TEXT NOT NULL DEFAULT '[]'"); + } + }; + + public static final Migration MIGRATION_8_9 = new Migration(8, 9) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TootEntity` ADD COLUMN `descriptions` TEXT DEFAULT '[]'"); + } + }; + + public static final Migration MIGRATION_9_10 = new Migration(9, 10) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultPostPrivacy` INTEGER NOT NULL DEFAULT 1"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultMediaSensitivity` INTEGER NOT NULL DEFAULT 0"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `alwaysShowSensitiveMedia` INTEGER NOT NULL DEFAULT 0"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `mediaPreviewEnabled` INTEGER NOT NULL DEFAULT '1'"); + } + }; + + public static final Migration MIGRATION_10_11 = new Migration(10, 11) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("CREATE TABLE IF NOT EXISTS `TimelineAccountEntity` (" + + "`serverId` TEXT NOT NULL, " + + "`timelineUserId` INTEGER NOT NULL, " + + "`instance` TEXT NOT NULL, " + + "`localUsername` TEXT NOT NULL, " + + "`username` TEXT NOT NULL, " + + "`displayName` TEXT NOT NULL, " + + "`url` TEXT NOT NULL, " + + "`avatar` TEXT NOT NULL, " + + "`emojis` TEXT NOT NULL," + + "PRIMARY KEY(`serverId`, `timelineUserId`))"); + + database.execSQL("CREATE TABLE IF NOT EXISTS `TimelineStatusEntity` (" + + "`serverId` TEXT NOT NULL, " + + "`url` TEXT, " + + "`timelineUserId` INTEGER NOT NULL, " + + "`authorServerId` TEXT," + + "`instance` TEXT, " + + "`inReplyToId` TEXT, " + + "`inReplyToAccountId` TEXT, " + + "`content` TEXT, " + + "`createdAt` INTEGER NOT NULL, " + + "`emojis` TEXT, " + + "`reblogsCount` INTEGER NOT NULL, " + + "`favouritesCount` INTEGER NOT NULL, " + + "`reblogged` INTEGER NOT NULL, " + + "`favourited` INTEGER NOT NULL, " + + "`sensitive` INTEGER NOT NULL, " + + "`spoilerText` TEXT, " + + "`visibility` INTEGER, " + + "`attachments` TEXT, " + + "`mentions` TEXT, " + + "`application` TEXT, " + + "`reblogServerId` TEXT, " + + "`reblogAccountId` TEXT," + + " PRIMARY KEY(`serverId`, `timelineUserId`)," + + " FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) " + + "ON UPDATE NO ACTION ON DELETE NO ACTION )"); + database.execSQL("CREATE INDEX IF NOT EXISTS" + + "`index_TimelineStatusEntity_authorServerId_timelineUserId` " + + "ON `TimelineStatusEntity` (`authorServerId`, `timelineUserId`)"); + } + }; + + public static final Migration MIGRATION_11_12 = new Migration(11, 12) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + String defaultTabs = TabDataKt.HOME + ";" + + TabDataKt.NOTIFICATIONS + ";" + + TabDataKt.LOCAL + ";" + + TabDataKt.FEDERATED; + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `tabPreferences` TEXT NOT NULL DEFAULT '" + defaultTabs + "'"); + + database.execSQL("CREATE TABLE IF NOT EXISTS `ConversationEntity` (" + + "`accountId` INTEGER NOT NULL, " + + "`id` TEXT NOT NULL, " + + "`accounts` TEXT NOT NULL, " + + "`unread` INTEGER NOT NULL, " + + "`s_id` TEXT NOT NULL, " + + "`s_url` TEXT, " + + "`s_inReplyToId` TEXT, " + + "`s_inReplyToAccountId` TEXT, " + + "`s_account` TEXT NOT NULL, " + + "`s_content` TEXT NOT NULL, " + + "`s_createdAt` INTEGER NOT NULL, " + + "`s_emojis` TEXT NOT NULL, " + + "`s_favouritesCount` INTEGER NOT NULL, " + + "`s_favourited` INTEGER NOT NULL, " + + "`s_sensitive` INTEGER NOT NULL, " + + "`s_spoilerText` TEXT NOT NULL, " + + "`s_attachments` TEXT NOT NULL, " + + "`s_mentions` TEXT NOT NULL, " + + "`s_showingHiddenContent` INTEGER NOT NULL, " + + "`s_expanded` INTEGER NOT NULL, " + + "`s_collapsible` INTEGER NOT NULL, " + + "`s_collapsed` INTEGER NOT NULL, " + + "PRIMARY KEY(`id`, `accountId`))"); + + } + }; + + public static final Migration MIGRATION_12_13 = new Migration(12, 13) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + + database.execSQL("DROP TABLE IF EXISTS `TimelineAccountEntity`"); + database.execSQL("DROP TABLE IF EXISTS `TimelineStatusEntity`"); + + database.execSQL("CREATE TABLE IF NOT EXISTS `TimelineAccountEntity` (" + + "`serverId` TEXT NOT NULL, " + + "`timelineUserId` INTEGER NOT NULL, " + + "`localUsername` TEXT NOT NULL, " + + "`username` TEXT NOT NULL, " + + "`displayName` TEXT NOT NULL, " + + "`url` TEXT NOT NULL, " + + "`avatar` TEXT NOT NULL, " + + "`emojis` TEXT NOT NULL," + + "PRIMARY KEY(`serverId`, `timelineUserId`))"); + + database.execSQL("CREATE TABLE IF NOT EXISTS `TimelineStatusEntity` (" + + "`serverId` TEXT NOT NULL, " + + "`url` TEXT, " + + "`timelineUserId` INTEGER NOT NULL, " + + "`authorServerId` TEXT," + + "`inReplyToId` TEXT, " + + "`inReplyToAccountId` TEXT, " + + "`content` TEXT, " + + "`createdAt` INTEGER NOT NULL, " + + "`emojis` TEXT, " + + "`reblogsCount` INTEGER NOT NULL, " + + "`favouritesCount` INTEGER NOT NULL, " + + "`reblogged` INTEGER NOT NULL, " + + "`favourited` INTEGER NOT NULL, " + + "`sensitive` INTEGER NOT NULL, " + + "`spoilerText` TEXT, " + + "`visibility` INTEGER, " + + "`attachments` TEXT, " + + "`mentions` TEXT, " + + "`application` TEXT, " + + "`reblogServerId` TEXT, " + + "`reblogAccountId` TEXT," + + " PRIMARY KEY(`serverId`, `timelineUserId`)," + + " FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`) " + + "ON UPDATE NO ACTION ON DELETE NO ACTION )"); + database.execSQL("CREATE INDEX IF NOT EXISTS" + + "`index_TimelineStatusEntity_authorServerId_timelineUserId` " + + "ON `TimelineStatusEntity` (`authorServerId`, `timelineUserId`)"); + } + }; + + public static final Migration MIGRATION_10_13 = new Migration(10, 13) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + MIGRATION_11_12.migrate(database); + MIGRATION_12_13.migrate(database); + } + }; + + public static final Migration MIGRATION_13_14 = new Migration(13, 14) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFilter` TEXT NOT NULL DEFAULT '[]'"); + } + }; + + public static final Migration MIGRATION_14_15 = new Migration(14, 15) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `poll` TEXT"); + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_poll` TEXT"); + } + }; + + public static final Migration MIGRATION_15_16 = new Migration(15, 16) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsPolls` INTEGER NOT NULL DEFAULT 1"); + } + }; + + public static final Migration MIGRATION_16_17 = new Migration(16, 17) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TimelineAccountEntity` ADD COLUMN `bot` INTEGER NOT NULL DEFAULT 0"); + } + }; + + public static final Migration MIGRATION_17_18 = new Migration(17, 18) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `alwaysOpenSpoiler` INTEGER NOT NULL DEFAULT 0"); + } + }; + + public static final Migration MIGRATION_18_19 = new Migration(18, 19) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollOptions` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollOptionLength` INTEGER"); + + database.execSQL("ALTER TABLE `TootEntity` ADD COLUMN `poll` TEXT"); + } + }; + + public static final Migration MIGRATION_19_20 = new Migration(19, 20) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `bookmarked` INTEGER NOT NULL DEFAULT 0"); + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_bookmarked` INTEGER NOT NULL DEFAULT 0"); + } + + }; + + public static final Migration MIGRATION_20_21 = new Migration(20, 21) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `version` TEXT"); + } + }; + + public static final Migration MIGRATION_21_22 = new Migration(21, 22) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsFollowRequested` INTEGER NOT NULL DEFAULT 0"); + } + }; + + public static final Migration MIGRATION_22_23 = new Migration(22, 23) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `muted` INTEGER"); + } + }; + + public static final Migration MIGRATION_23_24 = new Migration(23, 24) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSubscriptions` INTEGER NOT NULL DEFAULT 1"); + } + }; + + public static final Migration MIGRATION_24_25 = new Migration(24, 25) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL( + "CREATE TABLE IF NOT EXISTS `DraftEntity` (" + + "`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`accountId` INTEGER NOT NULL, " + + "`inReplyToId` TEXT," + + "`content` TEXT," + + "`contentWarning` TEXT," + + "`sensitive` INTEGER NOT NULL," + + "`visibility` INTEGER NOT NULL," + + "`attachments` TEXT NOT NULL," + + "`poll` TEXT," + + "`failedToSend` INTEGER NOT NULL)" + ); + } + }; + + public static class Migration25_26 extends Migration { + + private final File oldDraftDirectory; + + public Migration25_26(@Nullable File oldDraftDirectory) { + super(25, 26); + this.oldDraftDirectory = oldDraftDirectory; + } + + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("DROP TABLE `TootEntity`"); + + if (oldDraftDirectory != null && oldDraftDirectory.isDirectory()) { + File[] oldDraftFiles = oldDraftDirectory.listFiles(); + if (oldDraftFiles != null) { + for (File file : oldDraftFiles) { + if (!file.isDirectory()) { + file.delete(); + } + } + } + + } + } + } + + public static final Migration MIGRATION_26_27 = new Migration(26, 27) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_muted` INTEGER NOT NULL DEFAULT 0"); + } + }; + + public static final Migration MIGRATION_27_28 = new Migration(27, 28) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + + database.execSQL("DROP TABLE IF EXISTS `TimelineAccountEntity`"); + database.execSQL("DROP TABLE IF EXISTS `TimelineStatusEntity`"); + + database.execSQL("CREATE TABLE IF NOT EXISTS `TimelineAccountEntity` (" + + "`serverId` TEXT NOT NULL," + + "`timelineUserId` INTEGER NOT NULL," + + "`localUsername` TEXT NOT NULL," + + "`username` TEXT NOT NULL," + + "`displayName` TEXT NOT NULL," + + "`url` TEXT NOT NULL," + + "`avatar` TEXT NOT NULL," + + "`emojis` TEXT NOT NULL," + + "`bot` INTEGER NOT NULL," + + "PRIMARY KEY(`serverId`, `timelineUserId`) )"); + + database.execSQL("CREATE TABLE IF NOT EXISTS `TimelineStatusEntity` (" + + "`serverId` TEXT NOT NULL," + + "`url` TEXT," + + "`timelineUserId` INTEGER NOT NULL," + + "`authorServerId` TEXT," + + "`inReplyToId` TEXT," + + "`inReplyToAccountId` TEXT," + + "`content` TEXT," + + "`createdAt` INTEGER NOT NULL," + + "`emojis` TEXT," + + "`reblogsCount` INTEGER NOT NULL," + + "`favouritesCount` INTEGER NOT NULL," + + "`reblogged` INTEGER NOT NULL," + + "`bookmarked` INTEGER NOT NULL," + + "`favourited` INTEGER NOT NULL," + + "`sensitive` INTEGER NOT NULL," + + "`spoilerText` TEXT NOT NULL," + + "`visibility` INTEGER NOT NULL," + + "`attachments` TEXT," + + "`mentions` TEXT," + + "`application` TEXT," + + "`reblogServerId` TEXT," + + "`reblogAccountId` TEXT," + + "`poll` TEXT," + + "`muted` INTEGER," + + "`expanded` INTEGER NOT NULL," + + "`contentCollapsed` INTEGER NOT NULL," + + "`contentShowing` INTEGER NOT NULL," + + "`pinned` INTEGER NOT NULL," + + "PRIMARY KEY(`serverId`, `timelineUserId`)," + + "FOREIGN KEY(`authorServerId`, `timelineUserId`) REFERENCES `TimelineAccountEntity`(`serverId`, `timelineUserId`)" + + "ON UPDATE NO ACTION ON DELETE NO ACTION )"); + + database.execSQL("CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_timelineUserId`" + + "ON `TimelineStatusEntity` (`authorServerId`, `timelineUserId`)"); + } + }; + + public static final Migration MIGRATION_28_29 = new Migration(28, 29) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_tags` TEXT"); + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `tags` TEXT"); + } + }; + + public static final Migration MIGRATION_29_30 = new Migration(29, 30) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `charactersReservedPerUrl` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `minPollDuration` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxPollDuration` INTEGER"); + } + }; + + public static final Migration MIGRATION_30_31 = new Migration(30, 31) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + + // no actual scheme change, but placeholder ids are now used differently so the cache needs to be cleared to avoid bugs + database.execSQL("DELETE FROM `TimelineAccountEntity`"); + database.execSQL("DELETE FROM `TimelineStatusEntity`"); + } + }; + + public static final Migration MIGRATION_31_32 = new Migration(31, 32) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsSignUps` INTEGER NOT NULL DEFAULT 1"); + } + }; + + public static final Migration MIGRATION_32_33 = new Migration(32, 33) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + + // ConversationEntity lost the s_collapsible column + // since SQLite does not support removing columns and it is just a cache table, we recreate the whole table. + database.execSQL("DROP TABLE `ConversationEntity`"); + database.execSQL("CREATE TABLE IF NOT EXISTS `ConversationEntity` (" + + "`accountId` INTEGER NOT NULL," + + "`id` TEXT NOT NULL," + + "`accounts` TEXT NOT NULL," + + "`unread` INTEGER NOT NULL," + + "`s_id` TEXT NOT NULL," + + "`s_url` TEXT," + + "`s_inReplyToId` TEXT," + + "`s_inReplyToAccountId` TEXT," + + "`s_account` TEXT NOT NULL," + + "`s_content` TEXT NOT NULL," + + "`s_createdAt` INTEGER NOT NULL," + + "`s_emojis` TEXT NOT NULL," + + "`s_favouritesCount` INTEGER NOT NULL," + + "`s_favourited` INTEGER NOT NULL," + + "`s_bookmarked` INTEGER NOT NULL," + + "`s_sensitive` INTEGER NOT NULL," + + "`s_spoilerText` TEXT NOT NULL," + + "`s_attachments` TEXT NOT NULL," + + "`s_mentions` TEXT NOT NULL," + + "`s_tags` TEXT," + + "`s_showingHiddenContent` INTEGER NOT NULL," + + "`s_expanded` INTEGER NOT NULL," + + "`s_collapsed` INTEGER NOT NULL," + + "`s_muted` INTEGER NOT NULL," + + "`s_poll` TEXT," + + "PRIMARY KEY(`id`, `accountId`))"); + } + }; + + public static final Migration MIGRATION_33_34 = new Migration(33, 34) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsUpdates` INTEGER NOT NULL DEFAULT 1"); + } + }; + + public static final Migration MIGRATION_34_35 = new Migration(34, 35) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `card` TEXT"); + } + }; + + public static final Migration MIGRATION_35_36 = new Migration(35, 36) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `oauthScopes` TEXT NOT NULL DEFAULT ''"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `unifiedPushUrl` TEXT NOT NULL DEFAULT ''"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `pushPubKey` TEXT NOT NULL DEFAULT ''"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `pushPrivKey` TEXT NOT NULL DEFAULT ''"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `pushAuth` TEXT NOT NULL DEFAULT ''"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `pushServerKey` TEXT NOT NULL DEFAULT ''"); + } + }; + + public static final Migration MIGRATION_36_37 = new Migration(36, 37) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `repliesCount` INTEGER NOT NULL DEFAULT 0"); + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_repliesCount` INTEGER NOT NULL DEFAULT 0"); + } + }; + + public static final Migration MIGRATION_37_38 = new Migration(37, 38) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + // database needs to be cleaned because the ConversationAccountEntity got a new attribute + database.execSQL("DELETE FROM `ConversationEntity`"); + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `order` INTEGER NOT NULL DEFAULT 0"); + + // timestamps are now serialized differently so all cache tables that contain them need to be cleaned + database.execSQL("DELETE FROM `TimelineStatusEntity`"); + } + }; + + public static final Migration MIGRATION_38_39 = new Migration(38, 39) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `clientId` TEXT"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `clientSecret` TEXT"); + } + }; + + public static final Migration MIGRATION_39_40 = new Migration(39, 40) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `videoSizeLimit` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `imageSizeLimit` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `imageMatrixLimit` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxMediaAttachments` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxFields` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxFieldNameLength` INTEGER"); + database.execSQL("ALTER TABLE `InstanceEntity` ADD COLUMN `maxFieldValueLength` INTEGER"); + } + }; + + public static final Migration MIGRATION_40_41 = new Migration(40, 41) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `DraftEntity` ADD COLUMN `scheduledAt` TEXT"); + } + }; + + public static final Migration MIGRATION_41_42 = new Migration(41, 42) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `DraftEntity` ADD COLUMN `language` TEXT"); + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `language` TEXT"); + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_language` TEXT"); + } + }; + + public static final Migration MIGRATION_42_43 = new Migration(42, 43) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultPostLanguage` TEXT NOT NULL DEFAULT ''"); + } + }; + + public static final Migration MIGRATION_43_44 = new Migration(43, 44) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `notificationsReports` INTEGER NOT NULL DEFAULT 1"); + } + }; + + public static final Migration MIGRATION_44_45 = new Migration(44, 45) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `editedAt` INTEGER"); + database.execSQL("ALTER TABLE `ConversationEntity` ADD COLUMN `s_editedAt` INTEGER"); + } + }; + + public static final Migration MIGRATION_45_46 = new Migration(45, 46) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `DraftEntity` ADD COLUMN `statusId` TEXT"); + } + }; + + public static final Migration MIGRATION_46_47 = new Migration(46, 47) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `DraftEntity` ADD COLUMN `failedToSendNew` INTEGER NOT NULL DEFAULT 0"); + } + }; + + public static final Migration MIGRATION_47_48 = new Migration(47, 48) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `TimelineStatusEntity` ADD COLUMN `filtered` TEXT"); + } + }; + + @DeleteColumn(tableName = "AccountEntity", columnName = "activeNotifications") + static class MIGRATION_49_50 implements AutoMigrationSpec { } + + /** + * TabData.TRENDING was renamed to TabData.TRENDING_TAGS, and the text + * representation was changed from "Trending" to "TrendingTags". + */ + public static final Migration MIGRATION_52_53 = new Migration(52, 53) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("UPDATE `AccountEntity` SET `tabpreferences` = REPLACE(tabpreferences, 'Trending:', 'TrendingTags:')"); + } + }; + + public static final Migration MIGRATION_54_56 = new Migration(54, 56) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `isShowHomeBoosts` INTEGER NOT NULL DEFAULT 1"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `isShowHomeReplies` INTEGER NOT NULL DEFAULT 1"); + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `isShowHomeSelfBoosts` INTEGER NOT NULL DEFAULT 1"); + } + }; + + public static final Migration MIGRATION_58_60 = new Migration(58, 60) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + // drop the old tables - they are only caches anyway + database.execSQL("DROP TABLE `TimelineStatusEntity`"); + database.execSQL("DROP TABLE `TimelineAccountEntity`"); + + // create the new tables + database.execSQL(""" + CREATE TABLE IF NOT EXISTS `TimelineAccountEntity` ( + `serverId` TEXT NOT NULL, + `tuskyAccountId` INTEGER NOT NULL, + `localUsername` TEXT NOT NULL, + `username` TEXT NOT NULL, + `displayName` TEXT NOT NULL, + `url` TEXT NOT NULL, + `avatar` TEXT NOT NULL, + `emojis` TEXT NOT NULL, + `bot` INTEGER NOT NULL, + PRIMARY KEY(`serverId`, `tuskyAccountId`) + )""" + ); + database.execSQL(""" + CREATE TABLE IF NOT EXISTS `TimelineStatusEntity` ( + `serverId` TEXT NOT NULL, + `url` TEXT, + `tuskyAccountId` INTEGER NOT NULL, + `authorServerId` TEXT NOT NULL, + `inReplyToId` TEXT, + `inReplyToAccountId` TEXT, + `content` TEXT NOT NULL, + `createdAt` INTEGER NOT NULL, + `editedAt` INTEGER, + `emojis` TEXT NOT NULL, + `reblogsCount` INTEGER NOT NULL, + `favouritesCount` INTEGER NOT NULL, + `repliesCount` INTEGER NOT NULL, + `reblogged` INTEGER NOT NULL, + `bookmarked` INTEGER NOT NULL, + `favourited` INTEGER NOT NULL, + `sensitive` INTEGER NOT NULL, + `spoilerText` TEXT NOT NULL, + `visibility` INTEGER NOT NULL, + `attachments` TEXT NOT NULL, + `mentions` TEXT NOT NULL, + `tags` TEXT NOT NULL, + `application` TEXT, + `poll` TEXT, + `muted` INTEGER NOT NULL, + `expanded` INTEGER NOT NULL, + `contentCollapsed` INTEGER NOT NULL, + `contentShowing` INTEGER NOT NULL, + `pinned` INTEGER NOT NULL, + `card` TEXT, `language` TEXT, + `filtered` TEXT NOT NULL, + PRIMARY KEY(`serverId`, `tuskyAccountId`), + FOREIGN KEY(`authorServerId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION + )""" + ); + database.execSQL( + "CREATE INDEX IF NOT EXISTS `index_TimelineStatusEntity_authorServerId_tuskyAccountId` ON `TimelineStatusEntity` (`authorServerId`, `tuskyAccountId`)" + ); + database.execSQL(""" + CREATE TABLE IF NOT EXISTS `HomeTimelineEntity` ( + `tuskyAccountId` INTEGER NOT NULL, + `id` TEXT NOT NULL, + `statusId` TEXT, + `reblogAccountId` TEXT, + `loading` INTEGER NOT NULL, + PRIMARY KEY(`id`, `tuskyAccountId`), + FOREIGN KEY(`statusId`, `tuskyAccountId`) REFERENCES `TimelineStatusEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION, + FOREIGN KEY(`reblogAccountId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION + )""" + ); + database.execSQL( + "CREATE INDEX IF NOT EXISTS `index_HomeTimelineEntity_statusId_tuskyAccountId` ON `HomeTimelineEntity` (`statusId`, `tuskyAccountId`)" + ); + database.execSQL( + "CREATE INDEX IF NOT EXISTS `index_HomeTimelineEntity_reblogAccountId_tuskyAccountId` ON `HomeTimelineEntity` (`reblogAccountId`, `tuskyAccountId`)" + ); + database.execSQL(""" + CREATE TABLE IF NOT EXISTS `NotificationReportEntity`( + `tuskyAccountId` INTEGER NOT NULL, + `serverId` TEXT NOT NULL, + `category` TEXT NOT NULL, + `statusIds` TEXT, + `createdAt` INTEGER NOT NULL, + `targetAccountId` TEXT, + PRIMARY KEY(`serverId`, `tuskyAccountId`), + FOREIGN KEY(`targetAccountId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION + )""" + ); + database.execSQL( + "CREATE INDEX IF NOT EXISTS `index_NotificationReportEntity_targetAccountId_tuskyAccountId` ON `NotificationReportEntity` (`targetAccountId`, `tuskyAccountId`)" + ); + database.execSQL(""" + CREATE TABLE IF NOT EXISTS `NotificationEntity` ( + `tuskyAccountId` INTEGER NOT NULL, + `type` TEXT, + `id` TEXT NOT NULL, + `accountId` TEXT, + `statusId` TEXT, + `reportId` TEXT, + `loading` INTEGER NOT NULL, + PRIMARY KEY(`id`, `tuskyAccountId`), + FOREIGN KEY(`accountId`, `tuskyAccountId`) REFERENCES `TimelineAccountEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION, + FOREIGN KEY(`statusId`, `tuskyAccountId`) REFERENCES `TimelineStatusEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION, + FOREIGN KEY(`reportId`, `tuskyAccountId`) REFERENCES `NotificationReportEntity`(`serverId`, `tuskyAccountId`) ON UPDATE NO ACTION ON DELETE NO ACTION + )""" + ); + database.execSQL( + "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_accountId_tuskyAccountId` ON `NotificationEntity` (`accountId`, `tuskyAccountId`)" + ); + database.execSQL( + "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_statusId_tuskyAccountId` ON `NotificationEntity` (`statusId`, `tuskyAccountId`)" + ); + database.execSQL( + "CREATE INDEX IF NOT EXISTS `index_NotificationEntity_reportId_tuskyAccountId` ON `NotificationEntity` (`reportId`, `tuskyAccountId`)" + ); + } + }; + + public static final Migration MIGRATION_60_62 = new Migration(60, 62) { + @Override + public void migrate(@NonNull SupportSQLiteDatabase database) { + database.execSQL("ALTER TABLE `AccountEntity` ADD COLUMN `defaultReplyPrivacy` INTEGER NOT NULL DEFAULT 2"); + } + }; +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt new file mode 100644 index 0000000..001dbbe --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/ConversationsDao.kt @@ -0,0 +1,41 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.keylesspalace.tusky.components.conversation.ConversationEntity + +@Dao +interface ConversationsDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(conversations: List<ConversationEntity>) + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insert(conversation: ConversationEntity) + + @Query("DELETE FROM ConversationEntity WHERE id = :id AND accountId = :accountId") + suspend fun delete(id: String, accountId: Long) + + @Query("SELECT * FROM ConversationEntity WHERE accountId = :accountId ORDER BY `order` ASC") + fun conversationsForAccount(accountId: Long): PagingSource<Int, ConversationEntity> + + @Query("DELETE FROM ConversationEntity WHERE accountId = :accountId") + suspend fun deleteForAccount(accountId: Long) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt new file mode 100644 index 0000000..6280cc5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/Converters.kt @@ -0,0 +1,216 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db + +import androidx.room.ProvidedTypeConverter +import androidx.room.TypeConverter +import com.keylesspalace.tusky.TabData +import com.keylesspalace.tusky.components.conversation.ConversationAccountEntity +import com.keylesspalace.tusky.createTabDataFromId +import com.keylesspalace.tusky.db.entity.DraftAttachment +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Card +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.FilterResult +import com.keylesspalace.tusky.entity.HashTag +import com.keylesspalace.tusky.entity.NewPoll +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.Status +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import java.net.URLDecoder +import java.net.URLEncoder +import java.util.Date +import javax.inject.Inject +import javax.inject.Singleton + +@OptIn(ExperimentalStdlibApi::class) +@ProvidedTypeConverter +@Singleton +class Converters @Inject constructor( + private val moshi: Moshi +) { + + @TypeConverter + fun jsonToEmojiList(emojiListJson: String?): List<Emoji> { + return emojiListJson?.let { moshi.adapter<List<Emoji>?>().fromJson(it) }.orEmpty() + } + + @TypeConverter + fun emojiListToJson(emojiList: List<Emoji>): String { + return moshi.adapter<List<Emoji>>().toJson(emojiList) + } + + @TypeConverter + fun visibilityToInt(visibility: Status.Visibility?): Int { + return visibility?.num ?: Status.Visibility.UNKNOWN.num + } + + @TypeConverter + fun intToVisibility(visibility: Int): Status.Visibility { + return Status.Visibility.byNum(visibility) + } + + @TypeConverter + fun stringToTabData(str: String?): List<TabData>? { + return str?.split(";") + ?.map { + val data = it.split(":") + createTabDataFromId( + data[0], + data.drop(1).map { s -> URLDecoder.decode(s, "UTF-8") } + ) + } + } + + @TypeConverter + fun tabDataToString(tabData: List<TabData>?): String? { + // List name may include ":" + return tabData?.joinToString(";") { + it.id + ":" + it.arguments.joinToString(":") { s -> URLEncoder.encode(s, "UTF-8") } + } + } + + @TypeConverter + fun accountToJson(account: ConversationAccountEntity?): String { + return moshi.adapter<ConversationAccountEntity?>().toJson(account) + } + + @TypeConverter + fun jsonToAccount(accountJson: String?): ConversationAccountEntity? { + return accountJson?.let { moshi.adapter<ConversationAccountEntity?>().fromJson(it) } + } + + @TypeConverter + fun accountListToJson(accountList: List<ConversationAccountEntity>): String { + return moshi.adapter<List<ConversationAccountEntity>>().toJson(accountList) + } + + @TypeConverter + fun jsonToAccountList(accountListJson: String?): List<ConversationAccountEntity> { + return accountListJson?.let { moshi.adapter<List<ConversationAccountEntity>?>().fromJson(it) }.orEmpty() + } + + @TypeConverter + fun attachmentListToJson(attachmentList: List<Attachment>): String { + return moshi.adapter<List<Attachment>>().toJson(attachmentList) + } + + @TypeConverter + fun jsonToAttachmentList(attachmentListJson: String?): List<Attachment> { + return attachmentListJson?.let { moshi.adapter<List<Attachment>?>().fromJson(it) }.orEmpty() + } + + @TypeConverter + fun mentionListToJson(mentionArray: List<Status.Mention>): String { + return moshi.adapter<List<Status.Mention>>().toJson(mentionArray) + } + + @TypeConverter + fun jsonToMentionArray(mentionListJson: String?): List<Status.Mention> { + return mentionListJson?.let { moshi.adapter<List<Status.Mention>?>().fromJson(it) }.orEmpty() + } + + @TypeConverter + fun tagListToJson(tagArray: List<HashTag>?): String { + return moshi.adapter<List<HashTag>?>().toJson(tagArray) + } + + @TypeConverter + fun jsonToTagArray(tagListJson: String?): List<HashTag>? { + return tagListJson?.let { moshi.adapter<List<HashTag>?>().fromJson(it) } + } + + @TypeConverter + fun dateToLong(date: Date?): Long? { + return date?.time + } + + @TypeConverter + fun longToDate(date: Long?): Date? { + return date?.let { Date(it) } + } + + @TypeConverter + fun pollToJson(poll: Poll?): String { + return moshi.adapter<Poll?>().toJson(poll) + } + + @TypeConverter + fun jsonToPoll(pollJson: String?): Poll? { + return pollJson?.let { moshi.adapter<Poll?>().fromJson(it) } + } + + @TypeConverter + fun newPollToJson(newPoll: NewPoll?): String { + return moshi.adapter<NewPoll?>().toJson(newPoll) + } + + @TypeConverter + fun jsonToNewPoll(newPollJson: String?): NewPoll? { + return newPollJson?.let { moshi.adapter<NewPoll?>().fromJson(it) } + } + + @TypeConverter + fun draftAttachmentListToJson(draftAttachments: List<DraftAttachment>): String { + return moshi.adapter<List<DraftAttachment>>().toJson(draftAttachments) + } + + @TypeConverter + fun jsonToDraftAttachmentList(draftAttachmentListJson: String?): List<DraftAttachment> { + return draftAttachmentListJson?.let { moshi.adapter<List<DraftAttachment>?>().fromJson(it) }.orEmpty() + } + + @TypeConverter + fun filterResultListToJson(filterResults: List<FilterResult>?): String { + return moshi.adapter<List<FilterResult>?>().toJson(filterResults) + } + + @TypeConverter + fun jsonToFilterResultList(filterResultListJson: String?): List<FilterResult>? { + return filterResultListJson?.let { moshi.adapter<List<FilterResult>?>().fromJson(it) } + } + + @TypeConverter + fun cardToJson(card: Card?): String { + return moshi.adapter<Card?>().toJson(card) + } + + @TypeConverter + fun jsonToCard(cardJson: String?): Card? { + return cardJson?.let { moshi.adapter<Card?>().fromJson(cardJson) } + } + + @TypeConverter + fun stringListToJson(list: List<String>?): String? { + return moshi.adapter<List<String>?>().toJson(list) + } + + @TypeConverter + fun jsonToStringList(listJson: String?): List<String>? { + return listJson?.let { moshi.adapter<List<String>?>().fromJson(it) } + } + + @TypeConverter + fun applicationToJson(application: Status.Application?): String { + return moshi.adapter<Status.Application?>().toJson(application) + } + + @TypeConverter + fun jsonToApplication(applicationJson: String?): Status.Application? { + return applicationJson?.let { moshi.adapter<Status.Application?>().fromJson(it) } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DatabaseCleaner.kt b/app/src/main/java/com/keylesspalace/tusky/db/DatabaseCleaner.kt new file mode 100644 index 0000000..0d2ee9c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/DatabaseCleaner.kt @@ -0,0 +1,66 @@ +/* Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db + +import androidx.room.withTransaction +import com.keylesspalace.tusky.db.entity.HomeTimelineEntity +import com.keylesspalace.tusky.db.entity.NotificationEntity +import com.keylesspalace.tusky.db.entity.NotificationReportEntity +import com.keylesspalace.tusky.db.entity.TimelineAccountEntity +import com.keylesspalace.tusky.db.entity.TimelineStatusEntity +import javax.inject.Inject + +class DatabaseCleaner @Inject constructor( + private val db: AppDatabase +) { + /** + * Cleans the [HomeTimelineEntity], [TimelineStatusEntity], [TimelineAccountEntity], [NotificationEntity] and [NotificationReportEntity] tables from old entries. + * Should be regularly run to prevent the database from growing too big. + * @param tuskyAccountId id of the account for which to clean tables + * @param timelineLimit how many timeline items to keep + * @param notificationLimit how many notifications to keep + */ + suspend fun cleanupOldData( + tuskyAccountId: Long, + timelineLimit: Int, + notificationLimit: Int + ) { + db.withTransaction { + // the order here is important - foreign key constraints must not be violated + db.notificationsDao().cleanupNotifications(tuskyAccountId, notificationLimit) + db.notificationsDao().cleanupReports(tuskyAccountId) + db.timelineDao().cleanupHomeTimeline(tuskyAccountId, timelineLimit) + db.timelineStatusDao().cleanupStatuses(tuskyAccountId) + db.timelineAccountDao().cleanupAccounts(tuskyAccountId) + } + } + + /** + * Deletes everything from the [HomeTimelineEntity], [TimelineStatusEntity], [TimelineAccountEntity], [NotificationEntity] and [NotificationReportEntity] tables for one user. + * Intended to be used when a user logs out. + * @param tuskyAccountId id of the account for which to clean tables + */ + suspend fun cleanupEverything(tuskyAccountId: Long) { + db.withTransaction { + // the order here is important - foreign key constraints must not be violated + db.notificationsDao().removeAllNotifications(tuskyAccountId) + db.notificationsDao().removeAllReports(tuskyAccountId) + db.timelineDao().removeAllHomeTimelineItems(tuskyAccountId) + db.timelineStatusDao().removeAllStatuses(tuskyAccountId) + db.timelineAccountDao().removeAllAccounts(tuskyAccountId) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/DraftsAlert.kt b/app/src/main/java/com/keylesspalace/tusky/db/DraftsAlert.kt new file mode 100644 index 0000000..39f46e9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/DraftsAlert.kt @@ -0,0 +1,101 @@ +/* Copyright 2023 Andi McClure + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db + +import android.content.Context +import android.content.DialogInterface +import android.util.Log +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.LifecycleCoroutineScope +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.drafts.DraftsActivity +import com.keylesspalace.tusky.db.dao.DraftDao +import javax.inject.Inject +import javax.inject.Singleton +import kotlinx.coroutines.launch + +/** + * This class manages an alert popup when a post has failed and been saved to drafts. + * It must be separately registered in each lifetime in which it is to appear, + * and it only appears if the post failure belongs to the current user. + */ + +private const val TAG = "DraftsAlert" + +@Singleton +class DraftsAlert @Inject constructor(db: AppDatabase) { + // For tracking when a media upload fails in the service + private val draftDao: DraftDao = db.draftDao() + + @Inject + lateinit var accountManager: AccountManager + + fun <T> observeInContext(context: T, showAlert: Boolean) where T : Context, T : LifecycleOwner { + accountManager.activeAccount?.let { activeAccount -> + val coroutineScope = context.lifecycleScope + + // Assume a single MainActivity, AccountActivity or DraftsActivity never sees more then one user id in its lifetime. + val activeAccountId = activeAccount.id + + val draftsNeedUserAlert = draftDao.draftsNeedUserAlert(activeAccountId) + + // observe ensures that this gets called at the most appropriate moment wrt the context lifecycle— + // at init, at next onResume, or immediately if the context is resumed already. + coroutineScope.launch { + if (showAlert) { + draftsNeedUserAlert.collect { count -> + Log.d(TAG, "User id $activeAccountId changed: Notification-worthy draft count $count") + if (count > 0) { + AlertDialog.Builder(context) + .setTitle(R.string.action_post_failed) + .setMessage( + context.resources.getQuantityString(R.plurals.action_post_failed_detail, count) + ) + .setPositiveButton(R.string.action_post_failed_show_drafts) { _: DialogInterface?, _: Int -> + clearDraftsAlert(coroutineScope, activeAccountId) // User looked at drafts + + val intent = DraftsActivity.newIntent(context) + context.startActivity(intent) + } + .setNegativeButton(R.string.action_post_failed_do_nothing) { _: DialogInterface?, _: Int -> + clearDraftsAlert(coroutineScope, activeAccountId) // User doesn't care + } + .show() + } + } + } else { + draftsNeedUserAlert.collect { + Log.d(TAG, "User id $activeAccountId: Clean out notification-worthy drafts") + clearDraftsAlert(coroutineScope, activeAccountId) + } + } + } + } ?: run { + Log.w(TAG, "Attempted to observe drafts, but there is no active account") + } + } + + /** + * Clear drafts alert for specified user + */ + private fun clearDraftsAlert(coroutineScope: LifecycleCoroutineScope, id: Long) { + coroutineScope.launch { + draftDao.draftsClearNeedUserAlert(id) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/dao/AccountDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/dao/AccountDao.kt new file mode 100644 index 0000000..b310231 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/dao/AccountDao.kt @@ -0,0 +1,35 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db.dao + +import androidx.room.Dao +import androidx.room.Delete +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.keylesspalace.tusky.db.entity.AccountEntity + +@Dao +interface AccountDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insertOrReplace(account: AccountEntity): Long + + @Delete + fun delete(account: AccountEntity) + + @Query("SELECT * FROM AccountEntity ORDER BY id ASC") + fun loadAll(): List<AccountEntity> +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/dao/DraftDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/dao/DraftDao.kt new file mode 100644 index 0000000..c0f80aa --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/dao/DraftDao.kt @@ -0,0 +1,51 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db.dao + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.keylesspalace.tusky.db.entity.DraftEntity +import kotlinx.coroutines.flow.Flow + +@Dao +interface DraftDao { + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun insertOrReplace(draft: DraftEntity) + + @Query("SELECT * FROM DraftEntity WHERE accountId = :accountId ORDER BY id ASC") + fun draftsPagingSource(accountId: Long): PagingSource<Int, DraftEntity> + + @Query("SELECT COUNT(*) FROM DraftEntity WHERE accountId = :accountId AND failedToSendNew = 1") + fun draftsNeedUserAlert(accountId: Long): Flow<Int> + + @Query( + "UPDATE DraftEntity SET failedToSendNew = 0 WHERE accountId = :accountId AND failedToSendNew = 1" + ) + suspend fun draftsClearNeedUserAlert(accountId: Long) + + @Query("SELECT * FROM DraftEntity WHERE accountId = :accountId") + suspend fun loadDrafts(accountId: Long): List<DraftEntity> + + @Query("DELETE FROM DraftEntity WHERE id = :id") + suspend fun delete(id: Int) + + @Query("SELECT * FROM DraftEntity WHERE id = :id") + suspend fun find(id: Int): DraftEntity? +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/dao/InstanceDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/dao/InstanceDao.kt new file mode 100644 index 0000000..9f665e4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/dao/InstanceDao.kt @@ -0,0 +1,42 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db.dao + +import androidx.room.Dao +import androidx.room.Query +import androidx.room.RewriteQueriesToDropUnusedColumns +import androidx.room.Upsert +import com.keylesspalace.tusky.db.entity.EmojisEntity +import com.keylesspalace.tusky.db.entity.InstanceEntity +import com.keylesspalace.tusky.db.entity.InstanceInfoEntity + +@Dao +interface InstanceDao { + + @Upsert(entity = InstanceEntity::class) + suspend fun upsert(instance: InstanceInfoEntity) + + @Upsert(entity = InstanceEntity::class) + suspend fun upsert(emojis: EmojisEntity) + + @RewriteQueriesToDropUnusedColumns + @Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1") + suspend fun getInstanceInfo(instance: String): InstanceInfoEntity? + + @RewriteQueriesToDropUnusedColumns + @Query("SELECT * FROM InstanceEntity WHERE instance = :instance LIMIT 1") + suspend fun getEmojiInfo(instance: String): EmojisEntity? +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/dao/NotificationsDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/dao/NotificationsDao.kt new file mode 100644 index 0000000..e429dc2 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/dao/NotificationsDao.kt @@ -0,0 +1,175 @@ +/* Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db.dao + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy.Companion.REPLACE +import androidx.room.Query +import com.keylesspalace.tusky.db.entity.NotificationDataEntity +import com.keylesspalace.tusky.db.entity.NotificationEntity +import com.keylesspalace.tusky.db.entity.NotificationReportEntity + +@Dao +abstract class NotificationsDao { + + @Insert(onConflict = REPLACE) + abstract suspend fun insertNotification(notificationEntity: NotificationEntity): Long + + @Insert(onConflict = REPLACE) + abstract suspend fun insertReport(notificationReportDataEntity: NotificationReportEntity): Long + + @Query( + """ +SELECT n.tuskyAccountId, n.type, n.id, n.loading, +a.serverId as 'a_serverId', a.tuskyAccountId as 'a_tuskyAccountId', +a.localUsername as 'a_localUsername', a.username as 'a_username', +a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar', +a.emojis as 'a_emojis', a.bot as 'a_bot', +s.serverId as 's_serverId', s.url as 's_url', s.tuskyAccountId as 's_tuskyAccountId', +s.authorServerId as 's_authorServerId', s.inReplyToId as 's_inReplyToId', s.inReplyToAccountId as 's_inReplyToAccountId', +s.content as 's_content', s.createdAt as 's_createdAt', s.editedAt as 's_editedAt', s.emojis as 's_emojis', s.reblogsCount as 's_reblogsCount', +s.favouritesCount as 's_favouritesCount', s.repliesCount as 's_repliesCount', s.reblogged as 's_reblogged', s.favourited as 's_favourited', +s.bookmarked as 's_bookmarked', s.sensitive as 's_sensitive', s.spoilerText as 's_spoilerText', s.visibility as 's_visibility', +s.mentions as 's_mentions', s.tags as 's_tags', s.application as 's_application', s.content as 's_content', s.attachments as 's_attachments', s.poll as 's_poll', +s.card as 's_card', s.muted as 's_muted', s.expanded as 's_expanded', s.contentShowing as 's_contentShowing', s.contentCollapsed as 's_contentCollapsed', +s.pinned as 's_pinned', s.language as 's_language', s.filtered as 's_filtered', +sa.serverId as 'sa_serverId', sa.tuskyAccountId as 'sa_tuskyAccountId', +sa.localUsername as 'sa_localUsername', sa.username as 'sa_username', +sa.displayName as 'sa_displayName', sa.url as 'sa_url', sa.avatar as 'sa_avatar', +sa.emojis as 'sa_emojis', sa.bot as 'sa_bot', +r.serverId as 'r_serverId', r.tuskyAccountId as 'r_tuskyAccountId', +r.category as 'r_category', r.statusIds as 'r_statusIds', +r.createdAt as 'r_createdAt', r.targetAccountId as 'r_targetAccountId', +ra.serverId as 'ra_serverId', ra.tuskyAccountId as 'ra_tuskyAccountId', +ra.localUsername as 'ra_localUsername', ra.username as 'ra_username', +ra.displayName as 'ra_displayName', ra.url as 'ra_url', ra.avatar as 'ra_avatar', +ra.emojis as 'ra_emojis', ra.bot as 'ra_bot' +FROM NotificationEntity n +LEFT JOIN TimelineAccountEntity a ON (n.tuskyAccountId = a.tuskyAccountId AND n.accountId = a.serverId) +LEFT JOIN TimelineStatusEntity s ON (n.tuskyAccountId = s.tuskyAccountId AND n.statusId = s.serverId) +LEFT JOIN TimelineAccountEntity sa ON (n.tuskyAccountId = sa.tuskyAccountId AND s.authorServerId = sa.serverId) +LEFT JOIN NotificationReportEntity r ON (n.tuskyAccountId = r.tuskyAccountId AND n.reportId = r.serverId) +LEFT JOIN TimelineAccountEntity ra ON (n.tuskyAccountId = ra.tuskyAccountId AND r.targetAccountId = ra.serverId) +WHERE n.tuskyAccountId = :tuskyAccountId +ORDER BY LENGTH(n.id) DESC, n.id DESC""" + ) + abstract fun getNotifications(tuskyAccountId: Long): PagingSource<Int, NotificationDataEntity> + + @Query( + """DELETE FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND id = :notificationId""" + ) + abstract suspend fun delete(tuskyAccountId: Long, notificationId: String): Int + + @Query( + """DELETE FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND + (LENGTH(id) < LENGTH(:maxId) OR LENGTH(id) == LENGTH(:maxId) AND id <= :maxId) +AND +(LENGTH(id) > LENGTH(:minId) OR LENGTH(id) == LENGTH(:minId) AND id >= :minId) + """ + ) + abstract suspend fun deleteRange(tuskyAccountId: Long, minId: String, maxId: String): Int + + @Query( + """DELETE FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId""" + ) + internal abstract suspend fun removeAllNotifications(tuskyAccountId: Long) + + /** + * Deletes all NotificationReportEntities for Tusky user with id [tuskyAccountId]. + * Warning: This can violate foreign key constraints if reports are still referenced in the NotificationEntity table. + */ + @Query( + """DELETE FROM NotificationReportEntity WHERE tuskyAccountId = :tuskyAccountId""" + ) + internal abstract suspend fun removeAllReports(tuskyAccountId: Long) + + @Query( + """DELETE FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId = :statusId""" + ) + abstract suspend fun deleteAllWithStatus(tuskyAccountId: Long, statusId: String) + + /** + * Remove all notifications from user with id [userId] unless they are admin notifications. + */ + @Query( + """DELETE FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND + statusId IN + (SELECT serverId FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId AND + (authorServerId == :userId OR accountId == :userId)) + AND type != "admin.sign_up" AND type != "admin.report" + """ + ) + abstract suspend fun removeAllByUser(tuskyAccountId: Long, userId: String) + + @Query( + """DELETE FROM NotificationEntity + WHERE tuskyAccountId = :tuskyAccountId AND statusId IN ( + SELECT serverId FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId AND authorServerId in + ( SELECT serverId FROM TimelineAccountEntity WHERE username LIKE '%@' || :instanceDomain + AND tuskyAccountId = :tuskyAccountId) + OR accountId IN ( SELECT serverId FROM TimelineAccountEntity WHERE username LIKE '%@' || :instanceDomain + AND tuskyAccountId = :tuskyAccountId) + )""" + ) + abstract suspend fun deleteAllFromInstance(tuskyAccountId: Long, instanceDomain: String) + + @Query("SELECT id FROM NotificationEntity WHERE tuskyAccountId = :accountId ORDER BY LENGTH(id) DESC, id DESC LIMIT 1") + abstract suspend fun getTopId(accountId: Long): String? + + @Query("SELECT id FROM NotificationEntity WHERE tuskyAccountId = :accountId AND type IS NULL ORDER BY LENGTH(id) DESC, id DESC LIMIT 1") + abstract suspend fun getTopPlaceholderId(accountId: Long): String? + + /** + * Cleans the NotificationEntity table from old entries. + * @param tuskyAccountId id of the account for which to clean tables + * @param limit how many timeline items to keep + */ + @Query( + """DELETE FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND id NOT IN + (SELECT id FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId ORDER BY LENGTH(id) DESC, id DESC LIMIT :limit) + """ + ) + internal abstract suspend fun cleanupNotifications(tuskyAccountId: Long, limit: Int) + + /** + * Cleans the NotificationReportEntity table from unreferenced entries. + * @param tuskyAccountId id of the account for which to clean the table + */ + @Query( + """DELETE FROM NotificationReportEntity WHERE tuskyAccountId = :tuskyAccountId + AND serverId NOT IN + (SELECT reportId FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId and reportId IS NOT NULL)""" + ) + internal abstract suspend fun cleanupReports(tuskyAccountId: Long) + + /** + * Returns the id directly above [id], or null if [id] is the id of the top item + */ + @Query( + "SELECT id FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND (LENGTH(:id) < LENGTH(id) OR (LENGTH(:id) = LENGTH(id) AND :id < id)) ORDER BY LENGTH(id) ASC, id ASC LIMIT 1" + ) + abstract suspend fun getIdAbove(tuskyAccountId: Long, id: String): String? + + /** + * Returns the ID directly below [id], or null if [id] is the ID of the bottom item + */ + @Query( + "SELECT id FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND (LENGTH(:id) > LENGTH(id) OR (LENGTH(:id) = LENGTH(id) AND :id > id)) ORDER BY LENGTH(id) DESC, id DESC LIMIT 1" + ) + abstract suspend fun getIdBelow(tuskyAccountId: Long, id: String): String? +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/dao/TimelineAccountDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/dao/TimelineAccountDao.kt new file mode 100644 index 0000000..700063e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/dao/TimelineAccountDao.kt @@ -0,0 +1,56 @@ +/* Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy.Companion.REPLACE +import androidx.room.Query +import com.keylesspalace.tusky.db.entity.TimelineAccountEntity + +@Dao +abstract class TimelineAccountDao { + + @Insert(onConflict = REPLACE) + abstract suspend fun insert(timelineAccountEntity: TimelineAccountEntity): Long + + @Query( + """SELECT * FROM TimelineAccountEntity a + WHERE a.serverId = :accountId + AND a.tuskyAccountId = :tuskyAccountId""" + ) + internal abstract suspend fun getAccount(tuskyAccountId: Long, accountId: String): TimelineAccountEntity? + + @Query("DELETE FROM TimelineAccountEntity WHERE tuskyAccountId = :tuskyAccountId") + abstract suspend fun removeAllAccounts(tuskyAccountId: Long) + + /** + * Cleans the TimelineAccountEntity table from accounts that are no longer referenced by either TimelineStatusEntity, HomeTimelineEntity or NotificationEntity + * @param tuskyAccountId id of the user account for which to clean timeline accounts + */ + @Query( + """DELETE FROM TimelineAccountEntity WHERE tuskyAccountId = :tuskyAccountId + AND serverId NOT IN + (SELECT authorServerId FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId) + AND serverId NOT IN + (SELECT reblogAccountId FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND reblogAccountId IS NOT NULL) + AND serverId NOT IN + (SELECT accountId FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND accountId IS NOT NULL) + AND serverId NOT IN + (SELECT targetAccountId FROM NotificationReportEntity WHERE tuskyAccountId = :tuskyAccountId AND targetAccountId IS NOT NULL)""" + ) + abstract suspend fun cleanupAccounts(tuskyAccountId: Long) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/dao/TimelineDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/dao/TimelineDao.kt new file mode 100644 index 0000000..7ae0aac --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/dao/TimelineDao.kt @@ -0,0 +1,169 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db.dao + +import androidx.paging.PagingSource +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy.Companion.REPLACE +import androidx.room.Query +import com.keylesspalace.tusky.db.entity.HomeTimelineData +import com.keylesspalace.tusky.db.entity.HomeTimelineEntity + +@Dao +abstract class TimelineDao { + + @Insert(onConflict = REPLACE) + abstract suspend fun insertHomeTimelineItem(item: HomeTimelineEntity): Long + + @Query( + """ +SELECT h.id, s.serverId, s.url, s.tuskyAccountId, +s.authorServerId, s.inReplyToId, s.inReplyToAccountId, s.createdAt, s.editedAt, +s.emojis, s.reblogsCount, s.favouritesCount, s.repliesCount, s.reblogged, s.favourited, s.bookmarked, s.sensitive, +s.spoilerText, s.visibility, s.mentions, s.tags, s.application, +s.content, s.attachments, s.poll, s.card, s.muted, s.expanded, s.contentShowing, s.contentCollapsed, s.pinned, s.language, s.filtered, +a.serverId as 'a_serverId', a.tuskyAccountId as 'a_tuskyAccountId', +a.localUsername as 'a_localUsername', a.username as 'a_username', +a.displayName as 'a_displayName', a.url as 'a_url', a.avatar as 'a_avatar', +a.emojis as 'a_emojis', a.bot as 'a_bot', +rb.serverId as 'rb_serverId', rb.tuskyAccountId 'rb_tuskyAccountId', +rb.localUsername as 'rb_localUsername', rb.username as 'rb_username', +rb.displayName as 'rb_displayName', rb.url as 'rb_url', rb.avatar as 'rb_avatar', +rb.emojis as 'rb_emojis', rb.bot as 'rb_bot', +h.loading +FROM HomeTimelineEntity h +LEFT JOIN TimelineStatusEntity s ON (h.statusId = s.serverId AND s.tuskyAccountId = :tuskyAccountId) +LEFT JOIN TimelineAccountEntity a ON (s.authorServerId = a.serverId AND a.tuskyAccountId = :tuskyAccountId) +LEFT JOIN TimelineAccountEntity rb ON (h.reblogAccountId = rb.serverId AND rb.tuskyAccountId = :tuskyAccountId) +WHERE h.tuskyAccountId = :tuskyAccountId +ORDER BY LENGTH(h.id) DESC, h.id DESC""" + ) + abstract fun getHomeTimeline(tuskyAccountId: Long): PagingSource<Int, HomeTimelineData> + + @Query( + """DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND + (LENGTH(id) < LENGTH(:maxId) OR LENGTH(id) == LENGTH(:maxId) AND id <= :maxId) +AND +(LENGTH(id) > LENGTH(:minId) OR LENGTH(id) == LENGTH(:minId) AND id >= :minId) + """ + ) + abstract suspend fun deleteRange(tuskyAccountId: Long, minId: String, maxId: String): Int + + /** + * Remove all home timeline items that are statuses or reblogs by the user with id [userId], including reblogs from other people. + * (e.g. because user was blocked) + */ + @Query( + """DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND + (statusId IN + (SELECT serverId FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId AND authorServerId == :userId) + OR reblogAccountId == :userId) + """ + ) + abstract suspend fun removeAllByUser(tuskyAccountId: Long, userId: String) + + /** + * Remove all home timeline items that are statuses or reblogs by the user with id [userId], but not reblogs from other users. + * (e.g. because user was unfollowed) + */ + @Query( + """DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND + ((statusId IN + (SELECT serverId FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId AND authorServerId == :userId) + AND reblogAccountId IS NULL) + OR reblogAccountId == :userId) + """ + ) + abstract suspend fun removeStatusesAndReblogsByUser(tuskyAccountId: Long, userId: String) + + @Query("DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId") + abstract suspend fun removeAllHomeTimelineItems(tuskyAccountId: Long) + + @Query( + """DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND id = :id""" + ) + abstract suspend fun deleteHomeTimelineItem(tuskyAccountId: Long, id: String) + + /** + * Deletes all hometimeline items that reference the status with it [statusId]. They can be regular statuses or reblogs. + */ + @Query( + """DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId = :statusId""" + ) + abstract suspend fun deleteAllWithStatus(tuskyAccountId: Long, statusId: String) + + /** + * Trims the HomeTimelineEntity table down to [limit] entries by deleting the oldest in case there are more than [limit]. + * @param tuskyAccountId id of the account for which to clean the home timeline + * @param limit how many timeline items to keep + */ + @Query( + """DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND id NOT IN + (SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId ORDER BY LENGTH(id) DESC, id DESC LIMIT :limit) + """ + ) + internal abstract suspend fun cleanupHomeTimeline(tuskyAccountId: Long, limit: Int) + + @Query( + """DELETE FROM HomeTimelineEntity +WHERE tuskyAccountId = :tuskyAccountId AND statusId IN ( +SELECT serverId FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId AND authorServerId in +( SELECT serverId FROM TimelineAccountEntity WHERE username LIKE '%@' || :instanceDomain +AND tuskyAccountId = :tuskyAccountId +))""" + ) + abstract suspend fun deleteAllFromInstance(tuskyAccountId: Long, instanceDomain: String) + + @Query( + "SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId ORDER BY LENGTH(id) DESC, id DESC LIMIT 1" + ) + abstract suspend fun getTopId(tuskyAccountId: Long): String? + + @Query( + "SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId IS NULL ORDER BY LENGTH(id) DESC, id DESC LIMIT 1" + ) + abstract suspend fun getTopPlaceholderId(tuskyAccountId: Long): String? + + /** + * Returns the id directly above [id], or null if [id] is the id of the top item + */ + @Query( + "SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND (LENGTH(:id) < LENGTH(id) OR (LENGTH(:id) = LENGTH(id) AND :id < id)) ORDER BY LENGTH(id) ASC, id ASC LIMIT 1" + ) + abstract suspend fun getIdAbove(tuskyAccountId: Long, id: String): String? + + /** + * Returns the ID directly below [id], or null if [id] is the ID of the bottom item + */ + @Query( + "SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND (LENGTH(:id) > LENGTH(id) OR (LENGTH(:id) = LENGTH(id) AND :id > id)) ORDER BY LENGTH(id) DESC, id DESC LIMIT 1" + ) + abstract suspend fun getIdBelow(tuskyAccountId: Long, id: String): String? + + @Query("SELECT COUNT(*) FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId") + abstract suspend fun getHomeTimelineItemCount(tuskyAccountId: Long): Int + + /** Developer tools: Find N most recent status IDs */ + @Query( + "SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId ORDER BY LENGTH(id) DESC, id DESC LIMIT :count" + ) + abstract suspend fun getMostRecentNHomeTimelineIds(tuskyAccountId: Long, count: Int): List<String> + + /** Developer tools: Convert a home timeline item to a placeholder */ + @Query("UPDATE HomeTimelineEntity SET statusId = NULL, reblogAccountId = NULL WHERE id = :serverId") + abstract suspend fun convertHomeTimelineItemToPlaceholder(serverId: String) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/dao/TimelineStatusDao.kt b/app/src/main/java/com/keylesspalace/tusky/db/dao/TimelineStatusDao.kt new file mode 100644 index 0000000..17dbe75 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/dao/TimelineStatusDao.kt @@ -0,0 +1,279 @@ +/* Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy.Companion.REPLACE +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.TypeConverters +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.Converters +import com.keylesspalace.tusky.db.entity.TimelineAccountEntity +import com.keylesspalace.tusky.db.entity.TimelineStatusEntity +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Card +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.HashTag +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.Status +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter + +@Dao +abstract class TimelineStatusDao( + private val db: AppDatabase +) { + + @Insert(onConflict = REPLACE) + abstract suspend fun insert(timelineStatusEntity: TimelineStatusEntity): Long + + @Transaction + open suspend fun getStatusWithAccount(tuskyAccountId: Long, statusId: String): Pair<TimelineStatusEntity, TimelineAccountEntity>? { + val status = getStatus(tuskyAccountId, statusId) ?: return null + val account = db.timelineAccountDao().getAccount(tuskyAccountId, status.authorServerId) ?: return null + return status to account + } + + @Query( + """ +SELECT * FROM TimelineStatusEntity s +WHERE s.serverId = :statusId +AND s.authorServerId IS NOT NULL +AND s.tuskyAccountId = :tuskyAccountId""" + ) + abstract suspend fun getStatus(tuskyAccountId: Long, statusId: String): TimelineStatusEntity? + + @OptIn(ExperimentalStdlibApi::class) + suspend fun update(tuskyAccountId: Long, status: Status, moshi: Moshi) { + update( + tuskyAccountId = tuskyAccountId, + statusId = status.id, + content = status.content, + editedAt = status.editedAt?.time, + emojis = moshi.adapter<List<Emoji>?>().toJson(status.emojis), + reblogsCount = status.reblogsCount, + favouritesCount = status.favouritesCount, + repliesCount = status.repliesCount, + reblogged = status.reblogged, + bookmarked = status.bookmarked, + favourited = status.favourited, + sensitive = status.sensitive, + spoilerText = status.spoilerText, + visibility = status.visibility, + attachments = moshi.adapter<List<Attachment>?>().toJson(status.attachments), + mentions = moshi.adapter<List<Status.Mention>?>().toJson(status.mentions), + tags = moshi.adapter<List<HashTag>?>().toJson(status.tags), + poll = moshi.adapter<Poll?>().toJson(status.poll), + muted = status.muted, + pinned = status.pinned, + card = moshi.adapter<Card?>().toJson(status.card), + language = status.language + ) + } + + @Query( + """UPDATE TimelineStatusEntity + SET content = :content, + editedAt = :editedAt, + emojis = :emojis, + reblogsCount = :reblogsCount, + favouritesCount = :favouritesCount, + repliesCount = :repliesCount, + reblogged = :reblogged, + bookmarked = :bookmarked, + favourited = :favourited, + sensitive = :sensitive, + spoilerText = :spoilerText, + visibility = :visibility, + attachments = :attachments, + mentions = :mentions, + tags = :tags, + poll = :poll, + muted = :muted, + pinned = :pinned, + card = :card, + language = :language + WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId""" + ) + @TypeConverters(Converters::class) + abstract suspend fun update( + tuskyAccountId: Long, + statusId: String, + content: String?, + editedAt: Long?, + emojis: String?, + reblogsCount: Int, + favouritesCount: Int, + repliesCount: Int, + reblogged: Boolean, + bookmarked: Boolean, + favourited: Boolean, + sensitive: Boolean, + spoilerText: String, + visibility: Status.Visibility, + attachments: String?, + mentions: String?, + tags: String?, + poll: String?, + muted: Boolean?, + pinned: Boolean, + card: String?, + language: String? + ) + + @Query( + """UPDATE TimelineStatusEntity SET bookmarked = :bookmarked +WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId""" + ) + abstract suspend fun setBookmarked(tuskyAccountId: Long, statusId: String, bookmarked: Boolean) + + @Query( + """UPDATE TimelineStatusEntity SET reblogged = :reblogged +WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId""" + ) + abstract suspend fun setReblogged(tuskyAccountId: Long, statusId: String, reblogged: Boolean) + + @Query("DELETE FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId") + abstract suspend fun removeAllStatuses(tuskyAccountId: Long) + + @Query( + """DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND id = :id""" + ) + abstract suspend fun deleteHomeTimelineItem(tuskyAccountId: Long, id: String) + + /** + * Deletes all hometimeline items that reference the status with it [statusId]. They can be regular statuses or reblogs. + */ + @Query( + """DELETE FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId = :statusId""" + ) + abstract suspend fun deleteAllWithStatus(tuskyAccountId: Long, statusId: String) + + /** + * Cleans the TimelineStatusEntity table from unreferenced status entries. + * @param tuskyAccountId id of the account for which to clean statuses + */ + @Query( + """DELETE FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId + AND serverId NOT IN + (SELECT statusId FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId IS NOT NULL) + AND serverId NOT IN + (SELECT statusId FROM NotificationEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId IS NOT NULL)""" + ) + internal abstract suspend fun cleanupStatuses(tuskyAccountId: Long) + + @Query( + """UPDATE TimelineStatusEntity SET poll = :poll +WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId""" + ) + abstract suspend fun setVoted(tuskyAccountId: Long, statusId: String, poll: String) + + @Query( + """UPDATE TimelineStatusEntity SET expanded = :expanded +WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId""" + ) + abstract suspend fun setExpanded(tuskyAccountId: Long, statusId: String, expanded: Boolean) + + @Query( + """UPDATE TimelineStatusEntity SET contentShowing = :contentShowing +WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId""" + ) + abstract suspend fun setContentShowing( + tuskyAccountId: Long, + statusId: String, + contentShowing: Boolean + ) + + @Query( + """UPDATE TimelineStatusEntity SET contentCollapsed = :contentCollapsed +WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId""" + ) + abstract suspend fun setContentCollapsed( + tuskyAccountId: Long, + statusId: String, + contentCollapsed: Boolean + ) + + @Query( + """UPDATE TimelineStatusEntity SET pinned = :pinned +WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId""" + ) + abstract suspend fun setPinned(tuskyAccountId: Long, statusId: String, pinned: Boolean) + + @Query( + """DELETE FROM HomeTimelineEntity +WHERE tuskyAccountId = :tuskyAccountId AND statusId IN ( +SELECT serverId FROM TimelineStatusEntity WHERE tuskyAccountId = :tuskyAccountId AND authorServerId in +( SELECT serverId FROM TimelineAccountEntity WHERE username LIKE '%@' || :instanceDomain +AND tuskyAccountId = :tuskyAccountId +))""" + ) + abstract suspend fun deleteAllFromInstance(tuskyAccountId: Long, instanceDomain: String) + + @Query( + "UPDATE TimelineStatusEntity SET filtered = '[]' WHERE tuskyAccountId = :tuskyAccountId AND serverId = :statusId" + ) + abstract suspend fun clearWarning(tuskyAccountId: Long, statusId: String): Int + + @Query( + "SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId ORDER BY LENGTH(id) DESC, id DESC LIMIT 1" + ) + abstract suspend fun getTopId(tuskyAccountId: Long): String? + + @Query( + "SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId IS NULL ORDER BY LENGTH(id) DESC, id DESC LIMIT 1" + ) + abstract suspend fun getTopPlaceholderId(tuskyAccountId: Long): String? + + /** + * Returns the id directly above [id], or null if [id] is the id of the top item + */ + @Query( + "SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND (LENGTH(:id) < LENGTH(id) OR (LENGTH(:id) = LENGTH(id) AND :id < id)) ORDER BY LENGTH(id) ASC, id ASC LIMIT 1" + ) + abstract suspend fun getIdAbove(tuskyAccountId: Long, id: String): String? + + /** + * Returns the ID directly below [id], or null if [id] is the ID of the bottom item + */ + @Query( + "SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND (LENGTH(:id) > LENGTH(id) OR (LENGTH(:id) = LENGTH(id) AND :id > id)) ORDER BY LENGTH(id) DESC, id DESC LIMIT 1" + ) + abstract suspend fun getIdBelow(tuskyAccountId: Long, id: String): String? + + /** + * Returns the id of the next placeholder after [id], or null if there is no placeholder. + */ + @Query( + "SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId AND statusId IS NULL AND (LENGTH(:id) > LENGTH(id) OR (LENGTH(:id) = LENGTH(id) AND :id > id)) ORDER BY LENGTH(id) DESC, id DESC LIMIT 1" + ) + abstract suspend fun getNextPlaceholderIdAfter(tuskyAccountId: Long, id: String): String? + + @Query("SELECT COUNT(*) FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId") + abstract suspend fun getHomeTimelineItemCount(tuskyAccountId: Long): Int + + /** Developer tools: Find N most recent status IDs */ + @Query( + "SELECT id FROM HomeTimelineEntity WHERE tuskyAccountId = :tuskyAccountId ORDER BY LENGTH(id) DESC, id DESC LIMIT :count" + ) + abstract suspend fun getMostRecentNStatusIds(tuskyAccountId: Long, count: Int): List<String> + + /** Developer tools: Convert a home timeline item to a placeholder */ + @Query("UPDATE HomeTimelineEntity SET statusId = NULL, reblogAccountId = NULL WHERE id = :serverId") + abstract suspend fun convertStatusToPlaceholder(serverId: String) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/entity/AccountEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/entity/AccountEntity.kt new file mode 100644 index 0000000..390c15f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/entity/AccountEntity.kt @@ -0,0 +1,152 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db.entity + +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.Index +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.keylesspalace.tusky.TabData +import com.keylesspalace.tusky.db.Converters +import com.keylesspalace.tusky.defaultTabs +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.Status + +@Entity( + indices = [ + Index( + value = ["domain", "accountId"], + unique = true + ) + ] +) +@TypeConverters(Converters::class) +data class AccountEntity( + @field:PrimaryKey(autoGenerate = true) var id: Long, + val domain: String, + var accessToken: String, + // nullable for backward compatibility + var clientId: String?, + // nullable for backward compatibility + var clientSecret: String?, + var isActive: Boolean, + var accountId: String = "", + var username: String = "", + var displayName: String = "", + var profilePictureUrl: String = "", + var notificationsEnabled: Boolean = true, + var notificationsMentioned: Boolean = true, + var notificationsFollowed: Boolean = true, + var notificationsFollowRequested: Boolean = false, + var notificationsReblogged: Boolean = true, + var notificationsFavorited: Boolean = true, + var notificationsPolls: Boolean = true, + var notificationsSubscriptions: Boolean = true, + var notificationsSignUps: Boolean = true, + var notificationsUpdates: Boolean = true, + var notificationsReports: Boolean = true, + var notificationSound: Boolean = true, + var notificationVibration: Boolean = true, + var notificationLight: Boolean = true, + var defaultPostPrivacy: Status.Visibility = Status.Visibility.PUBLIC, + var defaultReplyPrivacy: Status.Visibility = Status.Visibility.UNLISTED, + var defaultMediaSensitivity: Boolean = false, + var defaultPostLanguage: String = "", + var alwaysShowSensitiveMedia: Boolean = false, + /** True if content behind a content warning is shown by default */ + var alwaysOpenSpoiler: Boolean = false, + + /** + * True if the "Download media previews" preference is true. This implies + * that media previews are shown as well as downloaded. + */ + var mediaPreviewEnabled: Boolean = true, + /** + * ID of the last notification the user read on the Notification, list, and should be restored + * to view when the user returns to the list. + * + * May not be the ID of the most recent notification if the user has scrolled down the list. + */ + var lastNotificationId: String = "0", + /** + * ID of the most recent Mastodon notification that Tusky has fetched to show as an + * Android notification. + */ + @ColumnInfo(defaultValue = "0") + var notificationMarkerId: String = "0", + var emojis: List<Emoji> = emptyList(), + var tabPreferences: List<TabData> = defaultTabs(), + var notificationsFilter: String = "[\"follow_request\"]", + // Scope cannot be changed without re-login, so store it in case + // the scope needs to be changed in the future + var oauthScopes: String = "", + var unifiedPushUrl: String = "", + var pushPubKey: String = "", + var pushPrivKey: String = "", + var pushAuth: String = "", + var pushServerKey: String = "", + + /** + * ID of the status at the top of the visible list in the home timeline when the + * user navigated away. + */ + var lastVisibleHomeTimelineStatusId: String? = null, + + /** true if the connected Mastodon account is locked (has to manually approve all follow requests **/ + @ColumnInfo(defaultValue = "0") + var locked: Boolean = false, + + @ColumnInfo(defaultValue = "0") + var hasDirectMessageBadge: Boolean = false, + + var isShowHomeBoosts: Boolean = true, + var isShowHomeReplies: Boolean = true, + var isShowHomeSelfBoosts: Boolean = true +) { + + val identifier: String + get() = "$domain:$accountId" + + val fullName: String + get() = "@$username@$domain" + + fun logout() { + // deleting credentials so they cannot be used again + accessToken = "" + clientId = null + clientSecret = null + } + + fun isLoggedIn() = accessToken.isNotEmpty() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as AccountEntity + + if (id == other.id) return true + return domain == other.domain && accountId == other.accountId + } + + override fun hashCode(): Int { + var result = id.hashCode() + result = 31 * result + domain.hashCode() + result = 31 * result + accountId.hashCode() + return result + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/entity/DraftEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/entity/DraftEntity.kt new file mode 100644 index 0000000..1842343 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/entity/DraftEntity.kt @@ -0,0 +1,67 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db.entity + +import android.net.Uri +import android.os.Parcelable +import androidx.core.net.toUri +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.keylesspalace.tusky.db.Converters +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.NewPoll +import com.keylesspalace.tusky.entity.Status +import com.squareup.moshi.JsonClass +import kotlinx.parcelize.Parcelize + +@Entity +@TypeConverters(Converters::class) +data class DraftEntity( + @PrimaryKey(autoGenerate = true) val id: Int = 0, + val accountId: Long, + val inReplyToId: String?, + val content: String?, + val contentWarning: String?, + val sensitive: Boolean, + val visibility: Status.Visibility, + val attachments: List<DraftAttachment>, + val poll: NewPoll?, + val failedToSend: Boolean, + val failedToSendNew: Boolean, + val scheduledAt: String?, + val language: String?, + val statusId: String? +) + +@JsonClass(generateAdapter = true) +@Parcelize +data class DraftAttachment( + val uriString: String, + val description: String?, + val focus: Attachment.Focus?, + val type: Type +) : Parcelable { + val uri: Uri + get() = uriString.toUri() + + @JsonClass(generateAdapter = false) + enum class Type { + IMAGE, + VIDEO, + AUDIO + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/db/entity/HomeTimelineEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/entity/HomeTimelineEntity.kt new file mode 100644 index 0000000..ccbdb44 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/entity/HomeTimelineEntity.kt @@ -0,0 +1,68 @@ +/* Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db.entity + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index + +/** + * Entity to store an item on the home timeline. Can be a standalone status, a reblog, or a placeholder. + */ +@Entity( + primaryKeys = ["id", "tuskyAccountId"], + foreignKeys = ( + [ + ForeignKey( + entity = TimelineStatusEntity::class, + parentColumns = ["serverId", "tuskyAccountId"], + childColumns = ["statusId", "tuskyAccountId"] + ), + ForeignKey( + entity = TimelineAccountEntity::class, + parentColumns = ["serverId", "tuskyAccountId"], + childColumns = ["reblogAccountId", "tuskyAccountId"] + ) + ] + ), + indices = [ + Index("statusId", "tuskyAccountId"), + Index("reblogAccountId", "tuskyAccountId"), + ] +) +data class HomeTimelineEntity( + val tuskyAccountId: Long, + // the id by which the timeline is sorted + val id: String, + // the id of the status, null when a placeholder + val statusId: String?, + // the id of the account who reblogged the status, null if no reblog + val reblogAccountId: String?, + // only relevant when this is a placeholder + val loading: Boolean = false +) + +/** + * Helper class for queries that return HomeTimelineEntity including all references + */ +data class HomeTimelineData( + val id: String, + @Embedded val status: TimelineStatusEntity?, + @Embedded(prefix = "a_") val account: TimelineAccountEntity?, + @Embedded(prefix = "rb_") val reblogAccount: TimelineAccountEntity?, + val loading: Boolean +) diff --git a/app/src/main/java/com/keylesspalace/tusky/db/entity/InstanceEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/entity/InstanceEntity.kt new file mode 100644 index 0000000..fc8cde3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/entity/InstanceEntity.kt @@ -0,0 +1,69 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db.entity + +import androidx.room.Entity +import androidx.room.PrimaryKey +import androidx.room.TypeConverters +import com.keylesspalace.tusky.db.Converters +import com.keylesspalace.tusky.entity.Emoji + +@Entity +@TypeConverters(Converters::class) +data class InstanceEntity( + @PrimaryKey val instance: String, + val emojiList: List<Emoji>?, + val maximumTootCharacters: Int?, + val maxPollOptions: Int?, + val maxPollOptionLength: Int?, + val minPollDuration: Int?, + val maxPollDuration: Int?, + val charactersReservedPerUrl: Int?, + val version: String?, + val videoSizeLimit: Int?, + val imageSizeLimit: Int?, + val imageMatrixLimit: Int?, + val maxMediaAttachments: Int?, + val maxFields: Int?, + val maxFieldNameLength: Int?, + val maxFieldValueLength: Int?, + val translationEnabled: Boolean?, +) + +@TypeConverters(Converters::class) +data class EmojisEntity( + @PrimaryKey val instance: String, + val emojiList: List<Emoji>? +) + +data class InstanceInfoEntity( + @PrimaryKey val instance: String, + val maximumTootCharacters: Int?, + val maxPollOptions: Int?, + val maxPollOptionLength: Int?, + val minPollDuration: Int?, + val maxPollDuration: Int?, + val charactersReservedPerUrl: Int?, + val version: String?, + val videoSizeLimit: Int?, + val imageSizeLimit: Int?, + val imageMatrixLimit: Int?, + val maxMediaAttachments: Int?, + val maxFields: Int?, + val maxFieldNameLength: Int?, + val maxFieldValueLength: Int?, + val translationEnabled: Boolean?, +) diff --git a/app/src/main/java/com/keylesspalace/tusky/db/entity/NotificationEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/entity/NotificationEntity.kt new file mode 100644 index 0000000..bb47daa --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/entity/NotificationEntity.kt @@ -0,0 +1,107 @@ +/* Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db.entity + +import androidx.room.Embedded +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.TypeConverters +import com.keylesspalace.tusky.db.Converters +import com.keylesspalace.tusky.entity.Notification +import java.util.Date + +data class NotificationDataEntity( + // id of the account logged into Tusky this notifications belongs to + val tuskyAccountId: Long, + // null when placeholder + val type: Notification.Type?, + val id: String, + @Embedded(prefix = "a_") val account: TimelineAccountEntity?, + @Embedded(prefix = "s_") val status: TimelineStatusEntity?, + @Embedded(prefix = "sa_") val statusAccount: TimelineAccountEntity?, + @Embedded(prefix = "r_") val report: NotificationReportEntity?, + @Embedded(prefix = "ra_") val reportTargetAccount: TimelineAccountEntity?, + // relevant when it is a placeholder + val loading: Boolean = false +) + +@Entity( + primaryKeys = ["id", "tuskyAccountId"], + foreignKeys = ( + [ + ForeignKey( + entity = TimelineAccountEntity::class, + parentColumns = ["serverId", "tuskyAccountId"], + childColumns = ["accountId", "tuskyAccountId"] + ), + ForeignKey( + entity = TimelineStatusEntity::class, + parentColumns = ["serverId", "tuskyAccountId"], + childColumns = ["statusId", "tuskyAccountId"] + ), + ForeignKey( + entity = NotificationReportEntity::class, + parentColumns = ["serverId", "tuskyAccountId"], + childColumns = ["reportId", "tuskyAccountId"] + ) + ] + ), + indices = [ + Index("accountId", "tuskyAccountId"), + Index("statusId", "tuskyAccountId"), + Index("reportId", "tuskyAccountId"), + ] +) +@TypeConverters(Converters::class) +data class NotificationEntity( + // id of the account logged into Tusky this notifications belongs to + val tuskyAccountId: Long, + // null when placeholder + val type: Notification.Type?, + val id: String, + val accountId: String?, + val statusId: String?, + val reportId: String?, + // relevant when it is a placeholder + val loading: Boolean = false +) + +@Entity( + primaryKeys = ["serverId", "tuskyAccountId"], + foreignKeys = ( + [ + ForeignKey( + entity = TimelineAccountEntity::class, + parentColumns = ["serverId", "tuskyAccountId"], + childColumns = ["targetAccountId", "tuskyAccountId"] + ) + ] + ), + indices = [ + Index("targetAccountId", "tuskyAccountId"), + ] +) +@TypeConverters(Converters::class) +data class NotificationReportEntity( + // id of the account logged into Tusky this report belongs to + val tuskyAccountId: Long, + val serverId: String, + val category: String, + val statusIds: List<String>?, + val createdAt: Date, + val targetAccountId: String? +) diff --git a/app/src/main/java/com/keylesspalace/tusky/db/entity/TimelineAccountEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/entity/TimelineAccountEntity.kt new file mode 100644 index 0000000..12499db --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/entity/TimelineAccountEntity.kt @@ -0,0 +1,37 @@ +/* Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db.entity + +import androidx.room.Entity +import androidx.room.TypeConverters +import com.keylesspalace.tusky.db.Converters +import com.keylesspalace.tusky.entity.Emoji + +@Entity( + primaryKeys = ["serverId", "tuskyAccountId"] +) +@TypeConverters(Converters::class) +data class TimelineAccountEntity( + val serverId: String, + val tuskyAccountId: Long, + val localUsername: String, + val username: String, + val displayName: String, + val url: String, + val avatar: String, + val emojis: List<Emoji>, + val bot: Boolean +) diff --git a/app/src/main/java/com/keylesspalace/tusky/db/entity/TimelineStatusEntity.kt b/app/src/main/java/com/keylesspalace/tusky/db/entity/TimelineStatusEntity.kt new file mode 100644 index 0000000..2b377d6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/db/entity/TimelineStatusEntity.kt @@ -0,0 +1,87 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.db.entity + +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.TypeConverters +import com.keylesspalace.tusky.db.Converters +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Card +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.FilterResult +import com.keylesspalace.tusky.entity.HashTag +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.Status + +/** + * Entity for caching status data. Used within home timelines and notifications. + * The information if a status is a reblog is not stored here but in [HomeTimelineEntity]. + */ +@Entity( + primaryKeys = ["serverId", "tuskyAccountId"], + foreignKeys = ( + [ + ForeignKey( + entity = TimelineAccountEntity::class, + parentColumns = ["serverId", "tuskyAccountId"], + childColumns = ["authorServerId", "tuskyAccountId"] + ) + ] + ), + // Avoiding rescanning status table when accounts table changes. Recommended by Room(c). + indices = [Index("authorServerId", "tuskyAccountId")] +) +@TypeConverters(Converters::class) +data class TimelineStatusEntity( + // id never flips: we need it for sorting so it's a real id + val serverId: String, + val url: String?, + // our local id for the logged in user in case there are multiple accounts per instance + val tuskyAccountId: Long, + val authorServerId: String, + val inReplyToId: String?, + val inReplyToAccountId: String?, + val content: String, + val createdAt: Long, + val editedAt: Long?, + val emojis: List<Emoji>, + val reblogsCount: Int, + val favouritesCount: Int, + val repliesCount: Int, + val reblogged: Boolean, + val bookmarked: Boolean, + val favourited: Boolean, + val sensitive: Boolean, + val spoilerText: String, + val visibility: Status.Visibility, + val attachments: List<Attachment>, + val mentions: List<Status.Mention>, + val tags: List<HashTag>, + val application: Status.Application?, + // if it has a reblogged status, it's id is stored here + val poll: Poll?, + val muted: Boolean, + /** Also used as the "loading" attribute when this TimelineStatusEntity is a placeholder */ + val expanded: Boolean, + val contentCollapsed: Boolean, + val contentShowing: Boolean, + val pinned: Boolean, + val card: Card?, + val language: String?, + val filtered: List<FilterResult> +) diff --git a/app/src/main/java/com/keylesspalace/tusky/di/CoroutineScopeModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/CoroutineScopeModule.kt new file mode 100644 index 0000000..61f513f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/CoroutineScopeModule.kt @@ -0,0 +1,49 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.di + +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Qualifier +import javax.inject.Singleton +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob + +/** + * Scope for potentially long-running tasks that should outlive the viewmodel that + * started them. For example, if the API call to bookmark a status is taking a long + * time, that call should not be cancelled because the user has navigated away from + * the viewmodel that made the call. + * + * @see https://developer.android.com/topic/architecture/data-layer#make_an_operation_live_longer_than_the_screen + */ +@Retention(AnnotationRetention.BINARY) +@Qualifier +annotation class ApplicationScope + +@Module +@InstallIn(SingletonComponent::class) +object CoroutineScopeModule { + @ApplicationScope + @Provides + @Singleton + fun providesApplicationScope() = CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt new file mode 100644 index 0000000..b1ccfa4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/NetworkModule.kt @@ -0,0 +1,168 @@ +/* Copyright 2018 charlag + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.di + +import android.content.Context +import android.content.SharedPreferences +import android.os.Build +import android.util.Log +import at.connyduck.calladapter.networkresult.NetworkResultCallAdapterFactory +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.json.GuardedAdapter +import com.keylesspalace.tusky.network.InstanceSwitchAuthInterceptor +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.network.MediaUploadApi +import com.keylesspalace.tusky.settings.PrefKeys.HTTP_PROXY_ENABLED +import com.keylesspalace.tusky.settings.PrefKeys.HTTP_PROXY_PORT +import com.keylesspalace.tusky.settings.PrefKeys.HTTP_PROXY_SERVER +import com.keylesspalace.tusky.settings.ProxyConfiguration +import com.keylesspalace.tusky.util.getNonNullString +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapters.EnumJsonAdapter +import com.squareup.moshi.adapters.Rfc3339DateJsonAdapter +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import java.net.IDN +import java.net.InetSocketAddress +import java.net.Proxy +import java.util.Date +import java.util.concurrent.TimeUnit +import javax.inject.Singleton +import okhttp3.Cache +import okhttp3.OkHttp +import okhttp3.OkHttpClient +import okhttp3.logging.HttpLoggingInterceptor +import retrofit2.Retrofit +import retrofit2.converter.moshi.MoshiConverterFactory +import retrofit2.create + +/** + * Created by charlag on 3/24/18. + */ + +@Module +@InstallIn(SingletonComponent::class) +object NetworkModule { + + private const val TAG = "NetworkModule" + + @Provides + @Singleton + fun providesMoshi(): Moshi = Moshi.Builder() + .add(Date::class.java, Rfc3339DateJsonAdapter()) + .add(GuardedAdapter.ANNOTATION_FACTORY) + // Enum types with fallback value + .add( + Attachment.Type::class.java, + EnumJsonAdapter.create(Attachment.Type::class.java) + .withUnknownFallback(Attachment.Type.UNKNOWN) + ) + .add( + Notification.Type::class.java, + EnumJsonAdapter.create(Notification.Type::class.java) + .withUnknownFallback(Notification.Type.UNKNOWN) + ) + .add( + Status.Visibility::class.java, + EnumJsonAdapter.create(Status.Visibility::class.java) + .withUnknownFallback(Status.Visibility.UNKNOWN) + ) + .build() + + @Provides + @Singleton + fun providesHttpClient( + accountManager: AccountManager, + @ApplicationContext context: Context, + preferences: SharedPreferences + ): OkHttpClient { + val httpProxyEnabled = preferences.getBoolean(HTTP_PROXY_ENABLED, false) + val httpServer = preferences.getNonNullString(HTTP_PROXY_SERVER, "") + val httpPort = preferences.getNonNullString(HTTP_PROXY_PORT, "-1").toIntOrNull() ?: -1 + val cacheSize = 25 * 1024 * 1024L // 25 MiB + val builder = OkHttpClient.Builder() + .addInterceptor { chain -> + /** + * Add a custom User-Agent that contains Tusky, Android and OkHttp Version to all requests + * Example: + * User-Agent: Tusky/1.1.2 Android/5.0.2 OkHttp/4.9.0 + * */ + val requestWithUserAgent = chain.request().newBuilder() + .header( + "User-Agent", + "Tusky/${BuildConfig.VERSION_NAME} Android/${Build.VERSION.RELEASE} OkHttp/${OkHttp.VERSION}" + ) + .build() + chain.proceed(requestWithUserAgent) + } + .readTimeout(30, TimeUnit.SECONDS) + .writeTimeout(30, TimeUnit.SECONDS) + .cache(Cache(context.cacheDir, cacheSize)) + + if (httpProxyEnabled) { + ProxyConfiguration.create(httpServer, httpPort)?.also { conf -> + val address = InetSocketAddress.createUnresolved(IDN.toASCII(conf.hostname), conf.port) + builder.proxy(Proxy(Proxy.Type.HTTP, address)) + } ?: Log.w(TAG, "Invalid proxy configuration: ($httpServer, $httpPort)") + } + + return builder + .apply { + addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) + if (BuildConfig.DEBUG) { + addInterceptor( + HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC } + ) + } + } + .build() + } + + @Provides + @Singleton + fun providesRetrofit(httpClient: OkHttpClient, moshi: Moshi): Retrofit { + return Retrofit.Builder().baseUrl("https://" + MastodonApi.PLACEHOLDER_DOMAIN) + .client(httpClient) + .addConverterFactory(MoshiConverterFactory.create(moshi)) + .addCallAdapterFactory(NetworkResultCallAdapterFactory.create()) + .build() + } + + @Provides + @Singleton + fun providesApi(retrofit: Retrofit): MastodonApi = retrofit.create() + + @Provides + @Singleton + fun providesMediaUploadApi(retrofit: Retrofit, okHttpClient: OkHttpClient): MediaUploadApi { + val longTimeOutOkHttpClient = okHttpClient.newBuilder() + .readTimeout(100, TimeUnit.SECONDS) + .writeTimeout(100, TimeUnit.SECONDS) + .build() + + return retrofit.newBuilder() + .client(longTimeOutOkHttpClient) + .build() + .create() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/PlayerModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/PlayerModule.kt new file mode 100644 index 0000000..ac5cf7b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/PlayerModule.kt @@ -0,0 +1,192 @@ +/* + * Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.di + +import android.content.Context +import android.os.Looper +import androidx.annotation.OptIn +import androidx.media3.common.C +import androidx.media3.common.Format +import androidx.media3.common.MimeTypes +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DefaultDataSource +import androidx.media3.datasource.okhttp.OkHttpDataSource +import androidx.media3.exoplayer.DefaultRenderersFactory +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.RenderersFactory +import androidx.media3.exoplayer.audio.AudioSink +import androidx.media3.exoplayer.audio.DefaultAudioSink +import androidx.media3.exoplayer.audio.MediaCodecAudioRenderer +import androidx.media3.exoplayer.mediacodec.MediaCodecSelector +import androidx.media3.exoplayer.metadata.MetadataRenderer +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.exoplayer.source.ProgressiveMediaSource +import androidx.media3.exoplayer.text.TextRenderer +import androidx.media3.exoplayer.video.MediaCodecVideoRenderer +import androidx.media3.extractor.ExtractorsFactory +import androidx.media3.extractor.flac.FlacExtractor +import androidx.media3.extractor.mkv.MatroskaExtractor +import androidx.media3.extractor.mp3.Mp3Extractor +import androidx.media3.extractor.mp4.FragmentedMp4Extractor +import androidx.media3.extractor.mp4.Mp4Extractor +import androidx.media3.extractor.ogg.OggExtractor +import androidx.media3.extractor.text.SubtitleParser +import androidx.media3.extractor.text.ttml.TtmlParser +import androidx.media3.extractor.text.webvtt.Mp4WebvttParser +import androidx.media3.extractor.text.webvtt.WebvttParser +import androidx.media3.extractor.wav.WavExtractor +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import okhttp3.OkHttpClient + +@Module +@InstallIn(SingletonComponent::class) +@OptIn(UnstableApi::class) +object PlayerModule { + @Provides + fun provideAudioSink(@ApplicationContext context: Context): AudioSink { + return DefaultAudioSink.Builder(context) + .build() + } + + @Provides + fun provideRenderersFactory( + @ApplicationContext context: Context, + audioSink: AudioSink + ): RenderersFactory { + return RenderersFactory { eventHandler, + videoRendererEventListener, + audioRendererEventListener, + textRendererOutput, + metadataRendererOutput -> + arrayOf( + MediaCodecVideoRenderer( + context, + MediaCodecSelector.DEFAULT, + DefaultRenderersFactory.DEFAULT_ALLOWED_VIDEO_JOINING_TIME_MS, + // enableDecoderFallback = true, helps playing videos even if one decoder fails + true, + eventHandler, + videoRendererEventListener, + DefaultRenderersFactory.MAX_DROPPED_VIDEO_FRAME_COUNT_TO_NOTIFY + ), + MediaCodecAudioRenderer( + context, + MediaCodecSelector.DEFAULT, + // enableDecoderFallback = true + true, + eventHandler, + audioRendererEventListener, + audioSink + ), + TextRenderer( + textRendererOutput, + eventHandler.looper + ), + MetadataRenderer( + metadataRendererOutput, + eventHandler.looper + ) + ) + } + } + + @Provides + fun providesSubtitleParserFactory(): SubtitleParser.Factory { + return object : SubtitleParser.Factory { + override fun supportsFormat(format: Format): Boolean { + return when (format.sampleMimeType) { + MimeTypes.TEXT_VTT, + MimeTypes.APPLICATION_MP4VTT, + MimeTypes.APPLICATION_TTML -> true + + else -> false + } + } + + override fun getCueReplacementBehavior(format: Format): Int { + return when (val mimeType = format.sampleMimeType) { + MimeTypes.TEXT_VTT -> WebvttParser.CUE_REPLACEMENT_BEHAVIOR + MimeTypes.APPLICATION_MP4VTT -> Mp4WebvttParser.CUE_REPLACEMENT_BEHAVIOR + MimeTypes.APPLICATION_TTML -> TtmlParser.CUE_REPLACEMENT_BEHAVIOR + else -> throw IllegalArgumentException("Unsupported MIME type: $mimeType") + } + } + + override fun create(format: Format): SubtitleParser { + return when (val mimeType = format.sampleMimeType) { + MimeTypes.TEXT_VTT -> WebvttParser() + MimeTypes.APPLICATION_MP4VTT -> Mp4WebvttParser() + MimeTypes.APPLICATION_TTML -> TtmlParser() + else -> throw IllegalArgumentException("Unsupported MIME type: $mimeType") + } + } + } + } + + @Provides + fun provideExtractorsFactory(subtitleParserFactory: SubtitleParser.Factory): ExtractorsFactory { + // Extractors order is optimized according to + // https://docs.google.com/document/d/1w2mKaWMxfz2Ei8-LdxqbPs1VLe_oudB-eryXXw9OvQQ + return ExtractorsFactory { + arrayOf( + FlacExtractor(), + WavExtractor(), + Mp4Extractor(subtitleParserFactory), + FragmentedMp4Extractor(subtitleParserFactory), + OggExtractor(), + MatroskaExtractor(subtitleParserFactory), + Mp3Extractor() + ) + } + } + + @Provides + fun provideDataSourceFactory( + @ApplicationContext context: Context, + okHttpClient: OkHttpClient + ): DataSource.Factory { + return DefaultDataSource.Factory(context, OkHttpDataSource.Factory(okHttpClient)) + } + + @Provides + fun provideMediaSourceFactory( + dataSourceFactory: DataSource.Factory, + extractorsFactory: ExtractorsFactory + ): MediaSource.Factory { + // Only progressive download is supported for Mastodon attachments + return ProgressiveMediaSource.Factory(dataSourceFactory, extractorsFactory) + } + + @Provides + fun provideExoPlayer( + @ApplicationContext context: Context, + renderersFactory: RenderersFactory, + mediaSourceFactory: MediaSource.Factory + ): ExoPlayer { + return ExoPlayer.Builder(context, renderersFactory, mediaSourceFactory) + .setLooper(Looper.getMainLooper()) + .setHandleAudioBecomingNoisy(true) // automatically pause when unplugging headphones + .setWakeMode(C.WAKE_MODE_NONE) // playback is always in the foreground + .build() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/PreferencesEntryPoint.kt b/app/src/main/java/com/keylesspalace/tusky/di/PreferencesEntryPoint.kt new file mode 100644 index 0000000..5ff1c31 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/PreferencesEntryPoint.kt @@ -0,0 +1,12 @@ +package com.keylesspalace.tusky.di + +import android.content.SharedPreferences +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent + +@EntryPoint +@InstallIn(SingletonComponent::class) +interface PreferencesEntryPoint { + fun preferences(): SharedPreferences +} diff --git a/app/src/main/java/com/keylesspalace/tusky/di/StorageModule.kt b/app/src/main/java/com/keylesspalace/tusky/di/StorageModule.kt new file mode 100644 index 0000000..5f5893b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/di/StorageModule.kt @@ -0,0 +1,72 @@ +/* Copyright 2018 charlag + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.di + +import android.content.Context +import android.content.SharedPreferences +import androidx.preference.PreferenceManager +import androidx.room.Room +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.Converters +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +/** + * Created by charlag on 3/21/18. + */ + +@Module +@InstallIn(SingletonComponent::class) +object StorageModule { + + @Provides + fun providesSharedPreferences(@ApplicationContext appContext: Context): SharedPreferences { + return PreferenceManager.getDefaultSharedPreferences(appContext) + } + + @Provides + @Singleton + fun providesDatabase(@ApplicationContext appContext: Context, converters: Converters): AppDatabase { + return Room.databaseBuilder(appContext, AppDatabase::class.java, "tuskyDB") + .addTypeConverter(converters) + .allowMainThreadQueries() + .addMigrations( + AppDatabase.MIGRATION_2_3, AppDatabase.MIGRATION_3_4, AppDatabase.MIGRATION_4_5, + AppDatabase.MIGRATION_5_6, AppDatabase.MIGRATION_6_7, AppDatabase.MIGRATION_7_8, + AppDatabase.MIGRATION_8_9, AppDatabase.MIGRATION_9_10, AppDatabase.MIGRATION_10_11, + AppDatabase.MIGRATION_11_12, AppDatabase.MIGRATION_12_13, AppDatabase.MIGRATION_10_13, + AppDatabase.MIGRATION_13_14, AppDatabase.MIGRATION_14_15, AppDatabase.MIGRATION_15_16, + AppDatabase.MIGRATION_16_17, AppDatabase.MIGRATION_17_18, AppDatabase.MIGRATION_18_19, + AppDatabase.MIGRATION_19_20, AppDatabase.MIGRATION_20_21, AppDatabase.MIGRATION_21_22, + AppDatabase.MIGRATION_22_23, AppDatabase.MIGRATION_23_24, AppDatabase.MIGRATION_24_25, + AppDatabase.Migration25_26(appContext.getExternalFilesDir("Tusky")), + AppDatabase.MIGRATION_26_27, AppDatabase.MIGRATION_27_28, AppDatabase.MIGRATION_28_29, + AppDatabase.MIGRATION_29_30, AppDatabase.MIGRATION_30_31, AppDatabase.MIGRATION_31_32, + AppDatabase.MIGRATION_32_33, AppDatabase.MIGRATION_33_34, AppDatabase.MIGRATION_34_35, + AppDatabase.MIGRATION_35_36, AppDatabase.MIGRATION_36_37, AppDatabase.MIGRATION_37_38, + AppDatabase.MIGRATION_38_39, AppDatabase.MIGRATION_39_40, AppDatabase.MIGRATION_40_41, + AppDatabase.MIGRATION_41_42, AppDatabase.MIGRATION_42_43, AppDatabase.MIGRATION_43_44, + AppDatabase.MIGRATION_44_45, AppDatabase.MIGRATION_45_46, AppDatabase.MIGRATION_46_47, + AppDatabase.MIGRATION_47_48, AppDatabase.MIGRATION_52_53, AppDatabase.MIGRATION_54_56, + AppDatabase.MIGRATION_58_60, AppDatabase.MIGRATION_60_62 + ) + .build() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt b/app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt new file mode 100644 index 0000000..150b4b4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/AccessToken.kt @@ -0,0 +1,24 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AccessToken( + @Json(name = "access_token") val accessToken: String +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt new file mode 100644 index 0000000..15e2f99 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Account.kt @@ -0,0 +1,85 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.Date + +@JsonClass(generateAdapter = true) +data class Account( + val id: String, + @Json(name = "username") val localUsername: String, + @Json(name = "acct") val username: String, + // should never be null per Api definition, but some servers break the contract + @Json(name = "display_name") val displayName: String? = null, + @Json(name = "created_at") val createdAt: Date, + val note: String, + val url: String, + val avatar: String, + val header: String, + val locked: Boolean = false, + @Json(name = "followers_count") val followersCount: Int = 0, + @Json(name = "following_count") val followingCount: Int = 0, + @Json(name = "statuses_count") val statusesCount: Int = 0, + val source: AccountSource? = null, + val bot: Boolean = false, + // default value for backward compatibility + val emojis: List<Emoji> = emptyList(), + // default value for backward compatibility + val fields: List<Field> = emptyList(), + val moved: Account? = null, + val roles: List<Role> = emptyList() +) { + + val name: String + get() = if (displayName.isNullOrEmpty()) { + localUsername + } else { + displayName + } + + val isRemote: Boolean + get() = this.username != this.localUsername +} + +@JsonClass(generateAdapter = true) +data class AccountSource( + val privacy: Status.Visibility = Status.Visibility.PUBLIC, + val sensitive: Boolean? = null, + val note: String? = null, + val fields: List<StringField> = emptyList(), + val language: String? = null +) + +@JsonClass(generateAdapter = true) +data class Field( + val name: String, + val value: String, + @Json(name = "verified_at") val verifiedAt: Date? = null +) + +@JsonClass(generateAdapter = true) +data class StringField( + val name: String, + val value: String +) + +@JsonClass(generateAdapter = true) +data class Role( + val name: String, + val color: String +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt new file mode 100644 index 0000000..792c242 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Announcement.kt @@ -0,0 +1,58 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.Date + +@JsonClass(generateAdapter = true) +data class Announcement( + val id: String, + val content: String, + @Json(name = "starts_at") val startsAt: Date? = null, + @Json(name = "ends_at") val endsAt: Date? = null, + @Json(name = "all_day") val allDay: Boolean, + @Json(name = "published_at") val publishedAt: Date, + @Json(name = "updated_at") val updatedAt: Date, + val read: Boolean = false, + val mentions: List<Status.Mention>, + val statuses: List<Status>, + val tags: List<HashTag>, + val emojis: List<Emoji>, + val reactions: List<Reaction> +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Announcement) return false + + return id == other.id + } + + override fun hashCode(): Int { + return id.hashCode() + } + + @JsonClass(generateAdapter = true) + data class Reaction( + val name: String, + val count: Int, + val me: Boolean = false, + val url: String? = null, + @Json(name = "static_url") val staticUrl: String? = null + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/AppCredentials.kt b/app/src/main/java/com/keylesspalace/tusky/entity/AppCredentials.kt new file mode 100644 index 0000000..5091413 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/AppCredentials.kt @@ -0,0 +1,25 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class AppCredentials( + @Json(name = "client_id") val clientId: String, + @Json(name = "client_secret") val clientSecret: String +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt new file mode 100644 index 0000000..553d6a8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Attachment.kt @@ -0,0 +1,90 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import android.os.Parcelable +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.parcelize.Parcelize + +@JsonClass(generateAdapter = true) +@Parcelize +data class Attachment( + val id: String, + val url: String, + // can be null for e.g. audio attachments + @Json(name = "preview_url") val previewUrl: String? = null, + val meta: MetaData? = null, + val type: Type, + val description: String? = null, + val blurhash: String? = null +) : Parcelable { + + @JsonClass(generateAdapter = false) + enum class Type { + @Json(name = "image") + IMAGE, + + @Json(name = "gifv") + GIFV, + + @Json(name = "video") + VIDEO, + + @Json(name = "audio") + AUDIO, + + UNKNOWN + } + + /** + * The meta data of an [Attachment]. + */ + @JsonClass(generateAdapter = true) + @Parcelize + data class MetaData( + val focus: Focus? = null, + val duration: Float? = null, + val original: Size? = null, + val small: Size? = null + ) : Parcelable + + /** + * The Focus entity, used to specify the focal point of an image. + * + * See here for more details what the x and y mean: + * https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point + */ + @JsonClass(generateAdapter = true) + @Parcelize + data class Focus( + val x: Float?, + val y: Float? + ) : Parcelable { + fun toMastodonApiString(): String = "$x,$y" + } + + /** + * The size of an image, used to specify the width/height. + */ + @JsonClass(generateAdapter = true) + @Parcelize + data class Size( + val width: Int = 0, + val height: Int = 0, + val aspect: Double = 0.0 + ) : Parcelable +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt new file mode 100644 index 0000000..baba8ce --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Card.kt @@ -0,0 +1,47 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Card( + val url: String, + val title: String, + val description: String = "", + @Json(name = "author_name") val authorName: String = "", + val image: String? = null, + val type: String, + val width: Int = 0, + val height: Int = 0, + val blurhash: String? = null, + @Json(name = "embed_url") val embedUrl: String? = null +) { + + override fun hashCode() = url.hashCode() + + override fun equals(other: Any?): Boolean { + if (other !is Card) { + return false + } + return other.url == this.url + } + + companion object { + const val TYPE_PHOTO = "photo" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Conversation.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Conversation.kt new file mode 100644 index 0000000..177073f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Conversation.kt @@ -0,0 +1,28 @@ +/* Copyright 2019 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Conversation( + val id: String, + val accounts: List<TimelineAccount>, + // should never be null, but apparently it's possible https://github.com/tuskyapp/Tusky/issues/1038 + @Json(name = "last_status") val lastStatus: Status? = null, + val unread: Boolean +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt new file mode 100644 index 0000000..eb6f5a6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/DeletedStatus.kt @@ -0,0 +1,36 @@ +/* Copyright 2019 Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.Date + +@JsonClass(generateAdapter = true) +data class DeletedStatus( + val text: String?, + @Json(name = "in_reply_to_id") val inReplyToId: String? = null, + @Json(name = "spoiler_text") val spoilerText: String, + val visibility: Status.Visibility, + val sensitive: Boolean, + @Json(name = "media_attachments") val attachments: List<Attachment>, + val poll: Poll? = null, + @Json(name = "created_at") val createdAt: Date, + val language: String? = null +) { + val isEmpty: Boolean + get() = text == null && attachments.isEmpty() +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt new file mode 100644 index 0000000..c4325a6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Emoji.kt @@ -0,0 +1,30 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import android.os.Parcelable +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.parcelize.Parcelize + +@JsonClass(generateAdapter = true) +@Parcelize +data class Emoji( + val shortcode: String, + val url: String, + @Json(name = "static_url") val staticUrl: String, + @Json(name = "visible_in_picker") val visibleInPicker: Boolean = true +) : Parcelable diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Error.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Error.kt new file mode 100644 index 0000000..5a170d5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Error.kt @@ -0,0 +1,28 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** @see [Error](https://docs.joinmastodon.org/entities/Error/) */ +@JsonClass(generateAdapter = true) +data class Error( + val error: String, + @Json(name = "error_description") val errorDescription: String? = null +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt new file mode 100644 index 0000000..f85be38 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Filter.kt @@ -0,0 +1,47 @@ +package com.keylesspalace.tusky.entity + +import android.os.Parcelable +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.Date +import kotlinx.parcelize.Parcelize + +@JsonClass(generateAdapter = true) +@Parcelize +data class Filter( + val id: String, + val title: String, + val context: List<String>, + @Json(name = "expires_at") val expiresAt: Date? = null, + @Json(name = "filter_action") val filterAction: String, + // This field is mandatory according to the API documentation but is in fact optional in some instances + val keywords: List<FilterKeyword> = emptyList(), + // val statuses: List<FilterStatus>, +) : Parcelable { + enum class Action(val action: String) { + NONE("none"), + WARN("warn"), + HIDE("hide"); + + companion object { + fun from(action: String): Action = entries.firstOrNull { it.action == action } ?: WARN + } + } + enum class Kind(val kind: String) { + HOME("home"), + NOTIFICATIONS("notifications"), + PUBLIC("public"), + THREAD("thread"), + ACCOUNT("account"); + + companion object { + fun from(kind: String): Kind = entries.firstOrNull { it.kind == kind } ?: PUBLIC + } + } + + val action: Action + get() = Action.from(filterAction) + + val kinds: List<Kind> + get() = context.map { Kind.from(it) } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/FilterKeyword.kt b/app/src/main/java/com/keylesspalace/tusky/entity/FilterKeyword.kt new file mode 100644 index 0000000..8947975 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/FilterKeyword.kt @@ -0,0 +1,14 @@ +package com.keylesspalace.tusky.entity + +import android.os.Parcelable +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.parcelize.Parcelize + +@JsonClass(generateAdapter = true) +@Parcelize +data class FilterKeyword( + val id: String, + val keyword: String, + @Json(name = "whole_word") val wholeWord: Boolean +) : Parcelable diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/FilterResult.kt b/app/src/main/java/com/keylesspalace/tusky/entity/FilterResult.kt new file mode 100644 index 0000000..c8ffa69 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/FilterResult.kt @@ -0,0 +1,10 @@ +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class FilterResult( + val filter: Filter, +// @Json(name = "keyword_matches") val keywordMatches: List<String>? = null, +// @Json(name = "status_matches") val statusMatches: List<String>? = null +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/FilterV1.kt b/app/src/main/java/com/keylesspalace/tusky/entity/FilterV1.kt new file mode 100644 index 0000000..7a8c9b1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/FilterV1.kt @@ -0,0 +1,66 @@ +/* Copyright 2018 Levi Bard + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.Date + +@JsonClass(generateAdapter = true) +data class FilterV1( + val id: String, + val phrase: String, + val context: List<String>, + @Json(name = "expires_at") val expiresAt: Date? = null, + val irreversible: Boolean, + @Json(name = "whole_word") val wholeWord: Boolean +) { + companion object { + const val HOME = "home" + const val NOTIFICATIONS = "notifications" + const val PUBLIC = "public" + const val THREAD = "thread" + const val ACCOUNT = "account" + } + + override fun hashCode(): Int { + return id.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other !is FilterV1) { + return false + } + return other.id == id + } + + fun toFilter(): Filter { + return Filter( + id = id, + title = phrase, + context = context, + expiresAt = expiresAt, + filterAction = Filter.Action.WARN.action, + keywords = listOf( + FilterKeyword( + id = id, + keyword = phrase, + wholeWord = wholeWord + ) + ) + ) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt b/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt new file mode 100644 index 0000000..384d6f4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/HashTag.kt @@ -0,0 +1,10 @@ +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class HashTag( + val name: String, + val url: String, + val following: Boolean? = null +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt new file mode 100644 index 0000000..92e71ba --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Instance.kt @@ -0,0 +1,98 @@ +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Instance( + val domain: String, +// val title: String, + val version: String, +// @Json(name = "source_url") val sourceUrl: String, +// val description: String, +// val usage: Usage, +// val thumbnail: Thumbnail, +// val languages: List<String>, + val configuration: Configuration? = null, +// val registrations: Registrations, +// val contact: Contact, + val rules: List<Rule> = emptyList(), + val pleroma: PleromaConfiguration? = null +) { + @JsonClass(generateAdapter = true) + data class Usage(val users: Users) { + @JsonClass(generateAdapter = true) + data class Users(@Json(name = "active_month") val activeMonth: Int) + } + + @JsonClass(generateAdapter = true) + data class Thumbnail( + val url: String, + val blurhash: String? = null, + val versions: Versions? = null + ) { + @JsonClass(generateAdapter = true) + data class Versions( + @Json(name = "@1x") val at1x: String? = null, + @Json(name = "@2x") val at2x: String? = null + ) + } + + @JsonClass(generateAdapter = true) + data class Configuration( + val urls: Urls? = null, + val accounts: Accounts? = null, + val statuses: Statuses? = null, + @Json(name = "media_attachments") val mediaAttachments: MediaAttachments? = null, + val polls: Polls? = null, + val translation: Translation? = null + ) { + @JsonClass(generateAdapter = true) + data class Urls(@Json(name = "streaming_api") val streamingApi: String? = null) + + @JsonClass(generateAdapter = true) + data class Accounts(@Json(name = "max_featured_tags") val maxFeaturedTags: Int) + + @JsonClass(generateAdapter = true) + data class Statuses( + @Json(name = "max_characters") val maxCharacters: Int? = null, + @Json(name = "max_media_attachments") val maxMediaAttachments: Int? = null, + @Json(name = "characters_reserved_per_url") val charactersReservedPerUrl: Int? = null + ) + + @JsonClass(generateAdapter = true) + data class MediaAttachments( + // Warning: This is an array in mastodon and a dictionary in friendica + // @Json(name = "supported_mime_types") val supportedMimeTypes: List<String> = emptyList(), + @Json(name = "image_size_limit") val imageSizeLimitBytes: Long? = null, + @Json(name = "image_matrix_limit") val imagePixelCountLimit: Long? = null, + @Json(name = "video_size_limit") val videoSizeLimitBytes: Long? = null, + @Json(name = "video_matrix_limit") val videoPixelCountLimit: Long? = null, + @Json(name = "video_frame_rate_limit") val videoFrameRateLimit: Int? = null + ) + + @JsonClass(generateAdapter = true) + data class Polls( + @Json(name = "max_options") val maxOptions: Int? = null, + @Json(name = "max_characters_per_option") val maxCharactersPerOption: Int? = null, + @Json(name = "min_expiration") val minExpirationSeconds: Int? = null, + @Json(name = "max_expiration") val maxExpirationSeconds: Int? = null + ) + + @JsonClass(generateAdapter = true) + data class Translation(val enabled: Boolean) + } + + @JsonClass(generateAdapter = true) + data class Registrations( + val enabled: Boolean, + @Json(name = "approval_required") val approvalRequired: Boolean, + val message: String? = null + ) + + @JsonClass(generateAdapter = true) + data class Contact(val email: String, val account: Account) + + @JsonClass(generateAdapter = true) + data class Rule(val id: String, val text: String) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/InstanceV1.kt b/app/src/main/java/com/keylesspalace/tusky/entity/InstanceV1.kt new file mode 100644 index 0000000..beddfdf --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/InstanceV1.kt @@ -0,0 +1,107 @@ +/* Copyright 2018 Levi Bard + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class InstanceV1( + val uri: String, + // val title: String, + // val description: String, + // val email: String, + val version: String, + // val urls: Map<String, String>, + // val stats: Map<String, Int>?, + // val thumbnail: String?, + // val languages: List<String>, + // @Json(name = "contact_account") val contactAccount: Account?, + @Json(name = "max_toot_chars") val maxTootChars: Int? = null, + @Json(name = "poll_limits") val pollConfiguration: PollConfiguration? = null, + val configuration: InstanceConfiguration? = null, + @Json(name = "max_media_attachments") val maxMediaAttachments: Int? = null, + val pleroma: PleromaConfiguration? = null, + @Json(name = "upload_limit") val uploadLimit: Int? = null, + val rules: List<InstanceRules> = emptyList() +) { + override fun hashCode(): Int { + return uri.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other !is InstanceV1) { + return false + } + return other.uri == uri + } +} + +@JsonClass(generateAdapter = true) +data class PollConfiguration( + @Json(name = "max_options") val maxOptions: Int? = null, + @Json(name = "max_option_chars") val maxOptionChars: Int? = null, + @Json(name = "max_characters_per_option") val maxCharactersPerOption: Int? = null, + @Json(name = "min_expiration") val minExpiration: Int? = null, + @Json(name = "max_expiration") val maxExpiration: Int? = null +) + +@JsonClass(generateAdapter = true) +data class InstanceConfiguration( + val statuses: StatusConfiguration? = null, + @Json(name = "media_attachments") val mediaAttachments: MediaAttachmentConfiguration? = null, + val polls: PollConfiguration? = null +) + +@JsonClass(generateAdapter = true) +data class StatusConfiguration( + @Json(name = "max_characters") val maxCharacters: Int? = null, + @Json(name = "max_media_attachments") val maxMediaAttachments: Int? = null, + @Json(name = "characters_reserved_per_url") val charactersReservedPerUrl: Int? = null +) + +@JsonClass(generateAdapter = true) +data class MediaAttachmentConfiguration( + @Json(name = "supported_mime_types") val supportedMimeTypes: List<String> = emptyList(), + @Json(name = "image_size_limit") val imageSizeLimit: Int? = null, + @Json(name = "image_matrix_limit") val imageMatrixLimit: Int? = null, + @Json(name = "video_size_limit") val videoSizeLimit: Int? = null, + @Json(name = "video_frame_rate_limit") val videoFrameRateLimit: Int? = null, + @Json(name = "video_matrix_limit") val videoMatrixLimit: Int? = null +) + +@JsonClass(generateAdapter = true) +data class PleromaConfiguration( + val metadata: PleromaMetadata? = null +) + +@JsonClass(generateAdapter = true) +data class PleromaMetadata( + @Json(name = "fields_limits") val fieldLimits: PleromaFieldLimits +) + +@JsonClass(generateAdapter = true) +data class PleromaFieldLimits( + @Json(name = "max_fields") val maxFields: Int? = null, + @Json(name = "name_length") val nameLength: Int? = null, + @Json(name = "value_length") val valueLength: Int? = null +) + +@JsonClass(generateAdapter = true) +data class InstanceRules( + val id: String, + val text: String +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt new file mode 100644 index 0000000..a1ecfb5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Marker.kt @@ -0,0 +1,17 @@ +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.Date + +/** + * API type for saving the scroll position of a timeline. + */ +@JsonClass(generateAdapter = true) +data class Marker( + @Json(name = "last_read_id") + val lastReadId: String, + val version: Int, + @Json(name = "updated_at") + val updatedAt: Date +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt b/app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt new file mode 100644 index 0000000..4a552a9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/MastoList.kt @@ -0,0 +1,42 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Created by charlag on 1/4/18. + */ +@JsonClass(generateAdapter = true) +data class MastoList( + val id: String, + val title: String, + val exclusive: Boolean? = null, + @Json(name = "replies_policy") val repliesPolicy: String? = null +) { + enum class ReplyPolicy(val policy: String) { + NONE("none"), + LIST("list"), + FOLLOWED("followed"); + + companion object { + fun from(policy: String?): ReplyPolicy = + entries.firstOrNull { it.policy == policy } ?: LIST + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/MediaUploadResult.kt b/app/src/main/java/com/keylesspalace/tusky/entity/MediaUploadResult.kt new file mode 100644 index 0000000..0202588 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/MediaUploadResult.kt @@ -0,0 +1,12 @@ +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.JsonClass + +/** + * The same as Attachment, except the url is null - see https://docs.joinmastodon.org/methods/statuses/media/ + * We are only interested in the id, so other attributes are omitted + */ +@JsonClass(generateAdapter = true) +data class MediaUploadResult( + val id: String +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt new file mode 100644 index 0000000..ec0de23 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/NewStatus.kt @@ -0,0 +1,54 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import android.os.Parcelable +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import kotlinx.parcelize.Parcelize + +@JsonClass(generateAdapter = true) +data class NewStatus( + val status: String, + @Json(name = "spoiler_text") val warningText: String, + @Json(name = "in_reply_to_id") val inReplyToId: String? = null, + val visibility: String, + val sensitive: Boolean, + @Json(name = "media_ids") val mediaIds: List<String> = emptyList(), + @Json(name = "media_attributes") val mediaAttributes: List<MediaAttribute> = emptyList(), + @Json(name = "scheduled_at") val scheduledAt: String? = null, + val poll: NewPoll? = null, + val language: String? = null +) + +@JsonClass(generateAdapter = true) +@Parcelize +data class NewPoll( + val options: List<String>, + @Json(name = "expires_in") val expiresIn: Int, + val multiple: Boolean +) : Parcelable + +// It would be nice if we could reuse MediaToSend, +// but the server requires a different format for focus +@JsonClass(generateAdapter = true) +@Parcelize +data class MediaAttribute( + val id: String, + val description: String? = null, + val focus: String? = null, + val thumbnail: String? = null +) : Parcelable diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt new file mode 100644 index 0000000..2fc8a25 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Notification.kt @@ -0,0 +1,104 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import androidx.annotation.StringRes +import com.keylesspalace.tusky.R +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Notification( + val type: Type, + val id: String, + val account: TimelineAccount, + val status: Status? = null, + val report: Report? = null +) { + + /** From https://docs.joinmastodon.org/entities/Notification/#type */ + @JsonClass(generateAdapter = false) + enum class Type(val presentation: String, @StringRes val uiString: Int) { + UNKNOWN("unknown", R.string.notification_unknown_name), + + /** Someone mentioned you */ + @Json(name = "mention") + MENTION("mention", R.string.notification_mention_name), + + /** Someone boosted one of your statuses */ + @Json(name = "reblog") + REBLOG("reblog", R.string.notification_boost_name), + + /** Someone favourited one of your statuses */ + @Json(name = "favourite") + FAVOURITE("favourite", R.string.notification_favourite_name), + + /** Someone followed you */ + @Json(name = "follow") + FOLLOW("follow", R.string.notification_follow_name), + + /** Someone requested to follow you */ + @Json(name = "follow_request") + FOLLOW_REQUEST("follow_request", R.string.notification_follow_request_name), + + /** A poll you have voted in or created has ended */ + @Json(name = "poll") + POLL("poll", R.string.notification_poll_name), + + /** Someone you enabled notifications for has posted a status */ + @Json(name = "status") + STATUS("status", R.string.notification_subscription_name), + + /** Someone signed up (optionally sent to admins) */ + @Json(name = "admin.sign_up") + SIGN_UP("admin.sign_up", R.string.notification_sign_up_name), + + /** A status you interacted with has been updated */ + @Json(name = "update") + UPDATE("update", R.string.notification_update_name), + + /** A new report has been filed */ + @Json(name = "admin.report") + REPORT("admin.report", R.string.notification_report_name); + + companion object { + fun byString(s: String): Type { + return entries.firstOrNull { it.presentation == s } ?: UNKNOWN + } + + /** Notification types for UI display (omits UNKNOWN) */ + val visibleTypes = + listOf(MENTION, REBLOG, FAVOURITE, FOLLOW, FOLLOW_REQUEST, POLL, STATUS, SIGN_UP, UPDATE, REPORT) + } + + override fun toString() = presentation + } + + // for Pleroma compatibility that uses Mention type + fun rewriteToStatusTypeIfNeeded(accountId: String): Notification { + if (type == Type.MENTION && status != null) { + return if (status.mentions.any { + it.id == accountId + } + ) { + this + } else { + copy(type = Type.STATUS) + } + } + return this + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/NotificationSubscribeResult.kt b/app/src/main/java/com/keylesspalace/tusky/entity/NotificationSubscribeResult.kt new file mode 100644 index 0000000..d0d84b9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/NotificationSubscribeResult.kt @@ -0,0 +1,26 @@ +/* Copyright 2022 Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class NotificationSubscribeResult( + val id: Int, + val endpoint: String, + @Json(name = "server_key") val serverKey: String +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Poll.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Poll.kt new file mode 100644 index 0000000..11d8ae9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Poll.kt @@ -0,0 +1,51 @@ +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.Date + +@JsonClass(generateAdapter = true) +data class Poll( + val id: String, + @Json(name = "expires_at") val expiresAt: Date? = null, + val expired: Boolean, + val multiple: Boolean, + @Json(name = "votes_count") val votesCount: Int, + // nullable for compatibility with Pleroma + @Json(name = "voters_count") val votersCount: Int? = null, + val options: List<PollOption>, + val voted: Boolean = false, + @Json(name = "own_votes") val ownVotes: List<Int> = emptyList() +) { + + fun votedCopy(choices: List<Int>): Poll { + val newOptions = options.mapIndexed { index, option -> + if (choices.contains(index)) { + option.copy(votesCount = (option.votesCount ?: 0) + 1) + } else { + option + } + } + + return copy( + options = newOptions, + votesCount = votesCount + choices.size, + votersCount = votersCount?.plus(1), + voted = true + ) + } + + fun toNewPoll(creationDate: Date) = NewPoll( + options.map { it.title }, + expiresAt?.let { + ((it.time - creationDate.time) / 1000).toInt() + 1 + } ?: 3600, + multiple + ) +} + +@JsonClass(generateAdapter = true) +data class PollOption( + val title: String, + @Json(name = "votes_count") val votesCount: Int? = null +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt new file mode 100644 index 0000000..3ad7f1f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Relationship.kt @@ -0,0 +1,42 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.keylesspalace.tusky.json.Guarded +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class Relationship( + val id: String, + val following: Boolean, + @Json(name = "followed_by") val followedBy: Boolean, + val blocking: Boolean, + val muting: Boolean, + @Json(name = "muting_notifications") val mutingNotifications: Boolean, + val requested: Boolean, + @Json(name = "showing_reblogs") val showingReblogs: Boolean, + /* Pleroma extension, same as 'notifying' on Mastodon. + * Some instances like qoto.org have a custom subscription feature where 'subscribing' is a json object, + * so we use GuardedAdapter to ignore the field if it is not a boolean. + */ + @Guarded val subscribing: Boolean? = null, + @Json(name = "domain_blocking") val blockingDomain: Boolean, + // nullable for backward compatibility / feature detection + val note: String? = null, + // since 3.3.0rc + val notifying: Boolean? = null +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Report.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Report.kt new file mode 100644 index 0000000..faac322 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Report.kt @@ -0,0 +1,14 @@ +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.Date + +@JsonClass(generateAdapter = true) +data class Report( + val id: String, + val category: String, + @Json(name = "status_ids") val statusIds: List<String>? = null, + @Json(name = "created_at") val createdAt: Date, + @Json(name = "target_account") val targetAccount: TimelineAccount +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/ScheduledStatus.kt b/app/src/main/java/com/keylesspalace/tusky/entity/ScheduledStatus.kt new file mode 100644 index 0000000..6be354d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/ScheduledStatus.kt @@ -0,0 +1,27 @@ +/* Copyright 2019 kyori19 + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class ScheduledStatus( + val id: String, + @Json(name = "scheduled_at") val scheduledAt: String, + val params: StatusParams, + @Json(name = "media_attachments") val mediaAttachments: List<Attachment> +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt b/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt new file mode 100644 index 0000000..27bce6d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/SearchResult.kt @@ -0,0 +1,25 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class SearchResult( + val accounts: List<TimelineAccount>, + val statuses: List<Status>, + val hashtags: List<HashTag> +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt new file mode 100644 index 0000000..ccf6170 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Status.kt @@ -0,0 +1,179 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import android.text.SpannableStringBuilder +import android.text.style.URLSpan +import com.keylesspalace.tusky.util.parseAsMastodonHtml +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.Date + +@JsonClass(generateAdapter = true) +data class Status( + val id: String, + // not present if it's reblog + val url: String? = null, + val account: TimelineAccount, + @Json(name = "in_reply_to_id") val inReplyToId: String? = null, + @Json(name = "in_reply_to_account_id") val inReplyToAccountId: String? = null, + val reblog: Status? = null, + val content: String, + @Json(name = "created_at") val createdAt: Date, + @Json(name = "edited_at") val editedAt: Date? = null, + val emojis: List<Emoji>, + @Json(name = "reblogs_count") val reblogsCount: Int, + @Json(name = "favourites_count") val favouritesCount: Int, + @Json(name = "replies_count") val repliesCount: Int, + val reblogged: Boolean = false, + val favourited: Boolean = false, + val bookmarked: Boolean = false, + val sensitive: Boolean, + @Json(name = "spoiler_text") val spoilerText: String, + val visibility: Visibility, + @Json(name = "media_attachments") val attachments: List<Attachment>, + val mentions: List<Mention>, + // Use null to mark the absence of tags because of semantic differences in LinkHelper + val tags: List<HashTag> = emptyList(), + val application: Application? = null, + val pinned: Boolean = false, + val muted: Boolean = false, + val poll: Poll? = null, + /** Preview card for links included within status content. */ + val card: Card? = null, + /** ISO 639 language code for this status. */ + val language: String? = null, + /** If the current token has an authorized user: The filter and keywords that matched this status. + * Iceshrimp and maybe other implementations explicitly send filtered=null so we can't default to empty list. */ + val filtered: List<FilterResult>? = null +) { + + val actionableId: String + get() = reblog?.id ?: id + + val actionableStatus: Status + get() = reblog ?: this + + @JsonClass(generateAdapter = false) + enum class Visibility(val num: Int) { + UNKNOWN(0), + + @Json(name = "public") + PUBLIC(1), + + @Json(name = "unlisted") + UNLISTED(2), + + @Json(name = "private") + PRIVATE(3), + + @Json(name = "direct") + DIRECT(4); + + val serverString: String + get() = when (this) { + PUBLIC -> "public" + UNLISTED -> "unlisted" + PRIVATE -> "private" + DIRECT -> "direct" + UNKNOWN -> "unknown" + } + + companion object { + + @JvmStatic + fun byNum(num: Int): Visibility { + return when (num) { + 4 -> DIRECT + 3 -> PRIVATE + 2 -> UNLISTED + 1 -> PUBLIC + 0 -> UNKNOWN + else -> UNKNOWN + } + } + + @JvmStatic + fun byString(s: String): Visibility { + return when (s) { + "public" -> PUBLIC + "unlisted" -> UNLISTED + "private" -> PRIVATE + "direct" -> DIRECT + "unknown" -> UNKNOWN + else -> UNKNOWN + } + } + } + } + + val isRebloggingAllowed: Boolean + get() { + return (visibility != Visibility.DIRECT && visibility != Visibility.UNKNOWN) + } + + fun toDeletedStatus(): DeletedStatus { + return DeletedStatus( + text = getEditableText(), + inReplyToId = inReplyToId, + spoilerText = spoilerText, + visibility = visibility, + sensitive = sensitive, + attachments = attachments, + poll = poll, + createdAt = createdAt, + language = language + ) + } + + private fun getEditableText(): String { + val contentSpanned = content.parseAsMastodonHtml() + val builder = SpannableStringBuilder(content.parseAsMastodonHtml()) + for (span in contentSpanned.getSpans(0, content.length, URLSpan::class.java)) { + val url = span.url + for ((_, url1, username) in mentions) { + if (url == url1) { + val start = builder.getSpanStart(span) + val end = builder.getSpanEnd(span) + if (start >= 0 && end >= 0) { + builder.replace(start, end, "@$username") + } + break + } + } + } + return builder.toString() + } + + @JsonClass(generateAdapter = true) + data class Mention( + val id: String, + val url: String, + @Json(name = "acct") val username: String, + @Json(name = "username") val localUsername: String + ) + + @JsonClass(generateAdapter = true) + data class Application( + val name: String, + val website: String? = null + ) + + companion object { + const val MAX_MEDIA_ATTACHMENTS = 4 + const val MAX_POLL_OPTIONS = 4 + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.kt b/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.kt new file mode 100644 index 0000000..35da031 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/StatusContext.kt @@ -0,0 +1,24 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class StatusContext( + val ancestors: List<Status>, + val descendants: List<Status> +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/StatusEdit.kt b/app/src/main/java/com/keylesspalace/tusky/entity/StatusEdit.kt new file mode 100644 index 0000000..0e922a7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/StatusEdit.kt @@ -0,0 +1,17 @@ +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass +import java.util.Date + +@JsonClass(generateAdapter = true) +data class StatusEdit( + val content: String, + @Json(name = "spoiler_text") val spoilerText: String, + val sensitive: Boolean, + @Json(name = "created_at") val createdAt: Date, + val account: TimelineAccount, + val poll: Poll? = null, + @Json(name = "media_attachments") val mediaAttachments: List<Attachment>, + val emojis: List<Emoji> +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/StatusParams.kt b/app/src/main/java/com/keylesspalace/tusky/entity/StatusParams.kt new file mode 100644 index 0000000..7c378d6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/StatusParams.kt @@ -0,0 +1,28 @@ +/* Copyright 2019 kyori19 + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class StatusParams( + val text: String, + val sensitive: Boolean? = null, + val visibility: Status.Visibility, + @Json(name = "spoiler_text") val spoilerText: String? = null, + @Json(name = "in_reply_to_id") val inReplyToId: String? = null +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/StatusSource.kt b/app/src/main/java/com/keylesspalace/tusky/entity/StatusSource.kt new file mode 100644 index 0000000..9b2fc97 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/StatusSource.kt @@ -0,0 +1,26 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class StatusSource( + val id: String, + val text: String, + @Json(name = "spoiler_text") val spoilerText: String +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/TimelineAccount.kt b/app/src/main/java/com/keylesspalace/tusky/entity/TimelineAccount.kt new file mode 100644 index 0000000..649c9ff --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/TimelineAccount.kt @@ -0,0 +1,46 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +/** + * Same as [Account], but only with the attributes required in timelines. + * Prefer this class over [Account] because it uses way less memory & deserializes faster from json. + */ +@JsonClass(generateAdapter = true) +data class TimelineAccount( + val id: String, + @Json(name = "username") val localUsername: String, + @Json(name = "acct") val username: String, + // should never be null per Api definition, but some servers break the contract + @Json(name = "display_name") val displayName: String? = null, + val url: String, + val avatar: String, + val note: String, + val bot: Boolean = false, + // optional for backward compatibility + val emojis: List<Emoji> = emptyList() +) { + + val name: String + get() = if (displayName.isNullOrEmpty()) { + localUsername + } else { + displayName + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/Translation.kt b/app/src/main/java/com/keylesspalace/tusky/entity/Translation.kt new file mode 100644 index 0000000..a9b7949 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/Translation.kt @@ -0,0 +1,38 @@ +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass + +@JsonClass(generateAdapter = true) +data class MediaTranslation( + val id: String, + val description: String, +) + +/** + * Represents the result of machine translating some status content. + * + * See [doc](https://docs.joinmastodon.org/entities/Translation/). + */ +@JsonClass(generateAdapter = true) +data class Translation( + val content: String, + @Json(name = "spoiler_text") + val spoilerText: String? = null, + val poll: TranslatedPoll? = null, + @Json(name = "media_attachments") + val mediaAttachments: List<MediaTranslation> = emptyList(), + @Json(name = "detected_source_language") + val detectedSourceLanguage: String, + val provider: String, +) + +@JsonClass(generateAdapter = true) +data class TranslatedPoll( + val options: List<TranslatedPollOption> +) + +@JsonClass(generateAdapter = true) +data class TranslatedPollOption( + val title: String +) diff --git a/app/src/main/java/com/keylesspalace/tusky/entity/TrendingTagsResult.kt b/app/src/main/java/com/keylesspalace/tusky/entity/TrendingTagsResult.kt new file mode 100644 index 0000000..e8cba0e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/entity/TrendingTagsResult.kt @@ -0,0 +1,52 @@ +/* Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.entity + +import com.squareup.moshi.JsonClass +import java.util.Date + +/** + * Mastodon API Documentation: https://docs.joinmastodon.org/methods/trends/#tags + * + * @param name The name of the hashtag (after the #). The "caturday" in "#caturday". + * (@param url The URL to your mastodon instance list for this hashtag.) + * @param history A list of [TrendingTagHistory]. Each element contains metrics per day for this hashtag. + * (@param following This is not listed in the APIs at the time of writing, but an instance is delivering it.) + */ +@JsonClass(generateAdapter = true) +data class TrendingTag( + val name: String, + val history: List<TrendingTagHistory> +) + +/** + * Mastodon API Documentation: https://docs.joinmastodon.org/methods/trends/#tags + * + * @param day The day that this was posted in Unix Epoch Seconds. + * @param accounts The number of accounts that have posted with this hashtag. + * @param uses The number of posts with this hashtag. + */ +@JsonClass(generateAdapter = true) +data class TrendingTagHistory( + val day: String, + val accounts: String, + val uses: String +) + +val TrendingTag.start + get() = Date(history.last().day.toLong() * 1000L) +val TrendingTag.end + get() = Date(history.first().day.toLong() * 1000L) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt new file mode 100644 index 0000000..11db2b8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/SFragment.kt @@ -0,0 +1,583 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ +package com.keylesspalace.tusky.fragment + +import android.Manifest +import android.app.DownloadManager +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.os.Environment +import android.util.Log +import android.view.MenuItem +import android.view.View +import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts +import androidx.annotation.LayoutRes +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.widget.PopupMenu +import androidx.core.app.ActivityOptionsCompat +import androidx.core.content.getSystemService +import androidx.fragment.app.Fragment +import androidx.lifecycle.lifecycleScope +import at.connyduck.calladapter.networkresult.fold +import at.connyduck.calladapter.networkresult.onFailure +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.BaseActivity +import com.keylesspalace.tusky.BottomSheetActivity +import com.keylesspalace.tusky.PostLookupFallbackBehavior +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.StatusListActivity.Companion.newHashtagIntent +import com.keylesspalace.tusky.ViewMediaActivity.Companion.newIntent +import com.keylesspalace.tusky.components.compose.ComposeActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity.Companion.startIntent +import com.keylesspalace.tusky.components.compose.ComposeActivity.ComposeOptions +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository +import com.keylesspalace.tusky.components.report.ReportActivity.Companion.getIntent +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.Translation +import com.keylesspalace.tusky.interfaces.AccountSelectionListener +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.usecase.TimelineCases +import com.keylesspalace.tusky.util.copyToClipboard +import com.keylesspalace.tusky.util.openLink +import com.keylesspalace.tusky.util.parseAsMastodonHtml +import com.keylesspalace.tusky.util.startActivityWithSlideInAnimation +import com.keylesspalace.tusky.view.showMuteAccountDialog +import com.keylesspalace.tusky.viewdata.AttachmentViewData +import java.util.Locale +import javax.inject.Inject +import kotlinx.coroutines.launch + +/* Note from Andrew on Jan. 22, 2017: This class is a design problem for me, so I left it with an + * awkward name. TimelineFragment and NotificationFragment have significant overlap but the nature + * of that is complicated by how they're coupled with Status and Notification and the corresponding + * adapters. I feel like the profile pages and thread viewer, which I haven't made yet, will also + * overlap functionality. So, I'm momentarily leaving it and hopefully working on those will clear + * up what needs to be where. */ +abstract class SFragment(@LayoutRes contentLayoutId: Int) : Fragment(contentLayoutId) { + protected abstract fun removeItem(position: Int) + protected abstract fun onReblog(reblog: Boolean, position: Int) + + /** `null` if translation is not supported on this screen */ + protected abstract val onMoreTranslate: ((translate: Boolean, position: Int) -> Unit)? + + private val bottomSheetActivity: BottomSheetActivity + get() = (requireActivity() as? BottomSheetActivity) + ?: throw IllegalStateException("Fragment must be attached to a BottomSheetActivity!") + + @Inject + lateinit var mastodonApi: MastodonApi + + @Inject + lateinit var accountManager: AccountManager + + @Inject + lateinit var timelineCases: TimelineCases + + @Inject + lateinit var instanceInfoRepository: InstanceInfoRepository + + private var pendingMediaDownloads: List<String>? = null + + private val downloadAllMediaPermissionLauncher = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) { + pendingMediaDownloads?.let { downloadAllMedia(it) } + } else { + Toast.makeText( + context, + R.string.error_media_download_permission, + Toast.LENGTH_SHORT + ).show() + } + pendingMediaDownloads = null + } + + override fun startActivity(intent: Intent) { + requireActivity().startActivityWithSlideInAnimation(intent) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + pendingMediaDownloads = savedInstanceState?.getStringArrayList(PENDING_MEDIA_DOWNLOADS_STATE_KEY) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + pendingMediaDownloads?.let { + outState.putStringArrayList(PENDING_MEDIA_DOWNLOADS_STATE_KEY, ArrayList(it)) + } + } + + override fun onResume() { + super.onResume() + + // make sure we have instance info for when we'll need it + instanceInfoRepository.precache() + } + + protected fun openReblog(status: Status?) { + if (status == null) return + bottomSheetActivity.viewAccount(status.account.id) + } + + protected fun viewThread(statusId: String?, statusUrl: String?) { + bottomSheetActivity.viewThread(statusId!!, statusUrl) + } + + protected fun viewAccount(accountId: String?) { + bottomSheetActivity.viewAccount(accountId!!) + } + + open fun onViewUrl(url: String) { + bottomSheetActivity.viewUrl(url, PostLookupFallbackBehavior.OPEN_IN_BROWSER) + } + + protected fun reply(status: Status) { + val actionableStatus = status.actionableStatus + val account = actionableStatus.account + var loggedInUsername: String? = null + val activeAccount = accountManager.activeAccount + if (activeAccount != null) { + loggedInUsername = activeAccount.username + } + val mentionedUsernames = LinkedHashSet( + listOf(account.username) + actionableStatus.mentions.map { it.username } + ).apply { remove(loggedInUsername) } + + val composeOptions = ComposeOptions( + inReplyToId = status.actionableId, + replyVisibility = actionableStatus.visibility, + contentWarning = actionableStatus.spoilerText, + mentionedUsernames = mentionedUsernames, + replyingStatusAuthor = account.localUsername, + replyingStatusContent = actionableStatus.content.parseAsMastodonHtml().toString(), + language = actionableStatus.language, + kind = ComposeActivity.ComposeKind.NEW + ) + + val intent = startIntent(requireContext(), composeOptions) + requireActivity().startActivity(intent) + } + + protected fun more(status: Status, view: View, position: Int, translation: Translation?) { + val id = status.actionableId + val accountId = status.actionableStatus.account.id + val accountUsername = status.actionableStatus.account.username + val statusUrl = status.actionableStatus.url + var loggedInAccountId: String? = null + val activeAccount = accountManager.activeAccount + if (activeAccount != null) { + loggedInAccountId = activeAccount.accountId + } + val popup = PopupMenu(requireContext(), view) + // Give a different menu depending on whether this is the user's own toot or not. + val statusIsByCurrentUser = loggedInAccountId != null && loggedInAccountId == accountId + if (statusIsByCurrentUser) { + popup.inflate(R.menu.status_more_for_user) + val menu = popup.menu + when (status.visibility) { + Status.Visibility.PUBLIC, Status.Visibility.UNLISTED -> { + menu.add( + 0, + R.id.pin, + 1, + getString( + if (status.pinned) R.string.unpin_action else R.string.pin_action + ) + ) + } + + Status.Visibility.PRIVATE -> { + val reblogged = status.reblog?.reblogged ?: status.reblogged + menu.findItem(R.id.status_reblog_private).isVisible = !reblogged + menu.findItem(R.id.status_unreblog_private).isVisible = reblogged + } + + else -> {} + } + } else { + popup.inflate(R.menu.status_more) + popup.menu.findItem(R.id.status_download_media).isVisible = + status.attachments.isNotEmpty() + } + val menu = popup.menu + val openAsItem = menu.findItem(R.id.status_open_as) + val openAsText = (activity as BaseActivity?)?.openAsText + if (openAsText == null) { + openAsItem.isVisible = false + } else { + openAsItem.title = openAsText + } + val muteConversationItem = menu.findItem(R.id.status_mute_conversation) + val mutable = + statusIsByCurrentUser || accountIsInMentions(activeAccount, status.mentions) + muteConversationItem.isVisible = mutable + if (mutable) { + muteConversationItem.setTitle( + if (!status.muted) { + R.string.action_mute_conversation + } else { + R.string.action_unmute_conversation + } + ) + } + + // translation not there for your own posts, posts already in your language or non-public posts + menu.findItem(R.id.status_translate)?.let { translateItem -> + translateItem.isVisible = onMoreTranslate != null && + !status.language.equals(Locale.getDefault().language, ignoreCase = true) && + instanceInfoRepository.cachedInstanceInfoOrFallback.translationEnabled == true && + (status.visibility == Status.Visibility.PUBLIC || status.visibility == Status.Visibility.UNLISTED) + translateItem.setTitle(if (translation != null) R.string.action_show_original else R.string.action_translate) + } + + popup.setOnMenuItemClickListener { item: MenuItem -> + when (item.itemId) { + R.id.post_share_content -> { + val statusToShare = status.reblog ?: status + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra( + Intent.EXTRA_TEXT, + "${statusToShare.account.username} - ${statusToShare.content.parseAsMastodonHtml()}" + ) + putExtra(Intent.EXTRA_SUBJECT, statusUrl) + } + startActivity( + Intent.createChooser( + sendIntent, + resources.getText(R.string.send_post_content_to) + ) + ) + return@setOnMenuItemClickListener true + } + + R.id.post_share_link -> { + val sendIntent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_TEXT, statusUrl) + type = "text/plain" + } + startActivity( + Intent.createChooser( + sendIntent, + resources.getText(R.string.send_post_link_to) + ) + ) + return@setOnMenuItemClickListener true + } + + R.id.status_copy_link -> { + statusUrl?.let { requireActivity().copyToClipboard(it, getString(R.string.url_copied)) } + return@setOnMenuItemClickListener true + } + + R.id.status_open_as -> { + showOpenAsDialog(statusUrl, item.title) + return@setOnMenuItemClickListener true + } + + R.id.status_download_media -> { + requestDownloadAllMedia(status) + return@setOnMenuItemClickListener true + } + + R.id.status_mute -> { + onMute(accountId, accountUsername) + return@setOnMenuItemClickListener true + } + + R.id.status_block -> { + onBlock(accountId, accountUsername) + return@setOnMenuItemClickListener true + } + + R.id.status_report -> { + openReportPage(accountId, accountUsername, id) + return@setOnMenuItemClickListener true + } + + R.id.status_unreblog_private -> { + onReblog(false, position) + return@setOnMenuItemClickListener true + } + + R.id.status_reblog_private -> { + onReblog(true, position) + return@setOnMenuItemClickListener true + } + + R.id.status_delete -> { + showConfirmDeleteDialog(id, position) + return@setOnMenuItemClickListener true + } + + R.id.status_delete_and_redraft -> { + showConfirmEditDialog(id, position, status) + return@setOnMenuItemClickListener true + } + + R.id.status_edit -> { + editStatus(id, status) + return@setOnMenuItemClickListener true + } + + R.id.pin -> { + viewLifecycleOwner.lifecycleScope.launch { + timelineCases.pin(status.id, !status.pinned) + .onFailure { e: Throwable -> + val message = e.message + ?: getString(if (status.pinned) R.string.failed_to_unpin else R.string.failed_to_pin) + Snackbar.make(requireView(), message, Snackbar.LENGTH_LONG) + .show() + } + } + return@setOnMenuItemClickListener true + } + + R.id.status_mute_conversation -> { + lifecycleScope.launch { + timelineCases.muteConversation(status.id, !status.muted) + } + return@setOnMenuItemClickListener true + } + + R.id.status_translate -> { + onMoreTranslate?.invoke(translation == null, position) + } + } + false + } + popup.show() + } + + private fun onMute(accountId: String, accountUsername: String) { + showMuteAccountDialog( + this.requireActivity(), + accountUsername + ) { notifications: Boolean, duration: Int? -> + lifecycleScope.launch { + timelineCases.mute(accountId, notifications, duration) + } + } + } + + private fun onBlock(accountId: String, accountUsername: String) { + AlertDialog.Builder(requireContext()) + .setMessage(getString(R.string.dialog_block_warning, accountUsername)) + .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> + lifecycleScope.launch { + timelineCases.block(accountId) + } + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + protected fun viewMedia(urlIndex: Int, attachments: List<AttachmentViewData>, view: View?) { + val (attachment) = attachments[urlIndex] + when (attachment.type) { + Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> { + val intent = newIntent(context, attachments, urlIndex) + if (view != null) { + val url = attachment.url + view.transitionName = url + val options = ActivityOptionsCompat.makeSceneTransitionAnimation( + requireActivity(), + view, + url + ) + startActivity(intent, options.toBundle()) + } else { + startActivity(intent) + } + } + + Attachment.Type.UNKNOWN -> { + requireContext().openLink(attachment.url) + } + } + } + + protected fun viewTag(tag: String) { + startActivity(newHashtagIntent(requireContext(), tag)) + } + + private fun openReportPage(accountId: String, accountUsername: String, statusId: String) { + startActivity(getIntent(requireContext(), accountId, accountUsername, statusId)) + } + + private fun showConfirmDeleteDialog(id: String, position: Int) { + AlertDialog.Builder(requireActivity()) + .setMessage(R.string.dialog_delete_post_warning) + .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> + viewLifecycleOwner.lifecycleScope.launch { + val result = timelineCases.delete(id).exceptionOrNull() + if (result != null) { + Log.w("SFragment", "error deleting status", result) + Toast.makeText(requireContext(), R.string.error_generic, Toast.LENGTH_SHORT).show() + } + // XXX: Removes the item even if there was an error. This is probably not + // correct (see similar code in showConfirmEditDialog() which only + // removes the item if the timelineCases.delete() call succeeded. + // + // Either way, this logic should be in the view model. + removeItem(position) + } + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun showConfirmEditDialog(id: String, position: Int, status: Status) { + val context = context ?: return + + AlertDialog.Builder(context) + .setMessage(R.string.dialog_redraft_post_warning) + .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> + viewLifecycleOwner.lifecycleScope.launch { + timelineCases.delete(id).fold( + { deletedStatus -> + removeItem(position) + val sourceStatus = if (deletedStatus.isEmpty) { + status.toDeletedStatus() + } else { + deletedStatus + } + val composeOptions = ComposeOptions( + content = sourceStatus.text, + inReplyToId = sourceStatus.inReplyToId, + visibility = sourceStatus.visibility, + contentWarning = sourceStatus.spoilerText, + mediaAttachments = sourceStatus.attachments, + sensitive = sourceStatus.sensitive, + modifiedInitialState = true, + language = sourceStatus.language, + poll = sourceStatus.poll?.toNewPoll(sourceStatus.createdAt), + kind = ComposeActivity.ComposeKind.NEW + ) + startActivity(startIntent(context, composeOptions)) + }, + { error: Throwable? -> + Log.w("SFragment", "error deleting status", error) + Toast.makeText(context, R.string.error_generic, Toast.LENGTH_SHORT) + .show() + } + ) + } + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } + + private fun editStatus(id: String, status: Status) { + viewLifecycleOwner.lifecycleScope.launch { + mastodonApi.statusSource(id).fold( + { source -> + val composeOptions = ComposeOptions( + content = source.text, + inReplyToId = status.inReplyToId, + visibility = status.visibility, + contentWarning = source.spoilerText, + mediaAttachments = status.attachments, + sensitive = status.sensitive, + language = status.language, + statusId = source.id, + poll = status.poll?.toNewPoll(status.createdAt), + kind = ComposeActivity.ComposeKind.EDIT_POSTED + ) + startActivity(startIntent(requireContext(), composeOptions)) + }, + { + Snackbar.make( + requireView(), + getString(R.string.error_status_source_load), + Snackbar.LENGTH_SHORT + ).show() + } + ) + } + } + + private fun showOpenAsDialog(statusUrl: String?, dialogTitle: CharSequence?) { + if (statusUrl == null) { + return + } + + (activity as BaseActivity).apply { + showAccountChooserDialog( + dialogTitle, + false, + object : AccountSelectionListener { + override fun onAccountSelected(account: AccountEntity) { + openAsAccount(statusUrl, account) + } + } + ) + } + } + + private fun downloadAllMedia(mediaUrls: List<String>) { + Toast.makeText(context, R.string.downloading_media, Toast.LENGTH_SHORT).show() + val downloadManager: DownloadManager = requireContext().getSystemService()!! + + for (url in mediaUrls) { + val uri = Uri.parse(url) + downloadManager.enqueue( + DownloadManager.Request(uri).apply { + setDestinationInExternalPublicDir( + Environment.DIRECTORY_DOWNLOADS, + uri.lastPathSegment + ) + } + ) + } + } + + private fun requestDownloadAllMedia(status: Status) { + if (status.attachments.isEmpty()) { + return + } + val mediaUrls = status.attachments.map { it.url } + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + pendingMediaDownloads = mediaUrls + downloadAllMediaPermissionLauncher.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE) + } else { + downloadAllMedia(mediaUrls) + } + } + + companion object { + private const val TAG = "SFragment" + private const val PENDING_MEDIA_DOWNLOADS_STATE_KEY = "pending_media_downloads" + + private fun accountIsInMentions( + account: AccountEntity?, + mentions: List<Status.Mention> + ): Boolean { + return mentions.any { mention -> + account?.username == mention.username && account.domain == Uri.parse(mention.url)?.host + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt new file mode 100644 index 0000000..9a62e1c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewImageFragment.kt @@ -0,0 +1,353 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.fragment + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint +import android.graphics.PointF +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.GestureDetector +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.lifecycle.lifecycleScope +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.FragmentViewImageBinding +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.util.getParcelableCompat +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import com.ortiz.touchview.OnTouchCoordinatesListener +import com.ortiz.touchview.TouchImageView +import kotlin.math.abs +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.launch + +class ViewImageFragment : ViewMediaFragment() { + interface PhotoActionsListener { + fun onBringUp() + fun onDismiss() + fun onPhotoTap() + } + + private val binding by viewBinding(FragmentViewImageBinding::bind) + + private val photoActionsListener: PhotoActionsListener + get() = requireActivity() as PhotoActionsListener + private var transition: CompletableDeferred<Unit>? = null + private var shouldStartTransition = false + + // Volatile: Image requests happen on background thread and we want to see updates to it + // immediately on another thread. Atomic is an overkill for such thing. + @Volatile + private var startedTransition = false + + override fun setupMediaView( + url: String, + previewUrl: String?, + description: String?, + showingDescription: Boolean + ) { + binding.photoView.transitionName = url + binding.mediaDescription.text = description + binding.captionSheet.visible(showingDescription) + + startedTransition = false + loadImageFromNetwork(url, previewUrl, binding.photoView) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + this.transition = CompletableDeferred() + return inflater.inflate(R.layout.fragment_view_image, container, false) + } + + @SuppressLint("ClickableViewAccessibility") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + val arguments = requireArguments() + val attachment = arguments.getParcelableCompat<Attachment>(ARG_ATTACHMENT) + this.shouldStartTransition = arguments.getBoolean(ARG_START_POSTPONED_TRANSITION) + val url: String? + var description: String? = null + + if (attachment != null) { + url = attachment.url + description = attachment.description + } else { + url = arguments.getString(ARG_SINGLE_IMAGE_URL) + if (url == null) { + throw IllegalArgumentException("attachment or image url has to be set") + } + } + + val singleTapDetector = GestureDetector( + requireContext(), + object : GestureDetector.SimpleOnGestureListener() { + override fun onDown(e: MotionEvent) = true + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { + photoActionsListener.onPhotoTap() + return false + } + } + ) + + binding.photoView.setOnTouchCoordinatesListener(object : OnTouchCoordinatesListener { + /** Y coordinate of the last single-finger drag */ + var lastDragY: Float? = null + + override fun onTouchCoordinate(view: View, event: MotionEvent, bitmapPoint: PointF) { + singleTapDetector.onTouchEvent(event) + + // Two fingers have gone down after a single finger drag. Finish the drag + if (event.pointerCount == 2 && lastDragY != null) { + onGestureEnd(view) + lastDragY = null + } + + // Stop the parent view from handling touches if either (a) the user has 2+ + // fingers on the screen, or (b) the image has been zoomed in, and can be scrolled + // horizontally in both directions. + // + // This stops things like ViewPager2 from trying to intercept a left/right swipe + // and ensures that the image does not appear to "stick" to the screen as different + // views fight over who should be handling the swipe. + // + // If the view can be scrolled in one direction it's OK to let the parent intercept, + // which allows the user to swipe between images even if one or more of them have + // been zoomed in. + if (event.pointerCount >= 2 || view.canScrollHorizontally(1) && view.canScrollHorizontally(-1)) { + when (event.action) { + MotionEvent.ACTION_DOWN, MotionEvent.ACTION_MOVE -> { + view.parent.requestDisallowInterceptTouchEvent(true) + } + + MotionEvent.ACTION_UP -> { + view.parent.requestDisallowInterceptTouchEvent(false) + } + } + return + } + + // The user is dragging the image around + if (event.pointerCount == 1) { + // If the image is zoomed then the swipe-to-dismiss functionality is disabled + if ((view as TouchImageView).isZoomed) return + + // The user's finger just went down, start recording where they are dragging from + if (event.action == MotionEvent.ACTION_DOWN) { + lastDragY = event.rawY + return + } + + // The user is dragging the un-zoomed image to possibly fling it up or down + // to dismiss. + if (event.action == MotionEvent.ACTION_MOVE) { + // lastDragY may be null; e.g., the user was performing a two-finger drag, + // and has lifted one finger. In this case do nothing + lastDragY ?: return + + // Compute the Y offset of the drag, and scale/translate the photoview + // accordingly. + val diff = event.rawY - lastDragY!! + if (view.translationY != 0f || abs(diff) > 40) { + // Drag has definitely started, stop the parent from interfering + view.parent.requestDisallowInterceptTouchEvent(true) + view.translationY += diff + val scale = (-abs(view.translationY) / 720 + 1).coerceAtLeast(0.5f) + view.scaleY = scale + view.scaleX = scale + lastDragY = event.rawY + } + return + } + + // The user has finished dragging. Allow the parent to handle touch events if + // appropriate, and end the gesture. + if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) { + view.parent.requestDisallowInterceptTouchEvent(false) + if (lastDragY != null) onGestureEnd(view) + lastDragY = null + return + } + } + } + + /** + * Handle the end of the user's gesture. + * + * If the user was previously dragging, and the image has been dragged a sufficient + * distance then we are done. Otherwise, animate the image back to its starting position. + */ + private fun onGestureEnd(view: View) { + if (abs(view.translationY) > 180) { + photoActionsListener.onDismiss() + } else { + view.animate().translationY(0f).scaleX(1f).scaleY(1f).start() + } + } + }) + + finalizeViewSetup(url, attachment?.previewUrl, description) + } + + override fun onToolbarVisibilityChange(visible: Boolean) { + if (view == null) return + + isDescriptionVisible = showingDescription && visible + val alpha = if (isDescriptionVisible) 1.0f else 0.0f + binding.captionSheet.animate().alpha(alpha) + .setListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + view ?: return + binding.captionSheet.visible(isDescriptionVisible) + animation.removeListener(this) + } + }) + .start() + } + + override fun onDestroyView() { + transition = null + super.onDestroyView() + } + + private fun loadImageFromNetwork(url: String, previewUrl: String?, photoView: ImageView) { + val glide = Glide.with(this) + // Request image from the any cache + glide + .load(url) + .dontAnimate() + .onlyRetrieveFromCache(true) + .let { + if (previewUrl != null) { + it.thumbnail( + glide + .load(previewUrl) + .dontAnimate() + .onlyRetrieveFromCache(true) + .centerInside() + .addListener(ImageRequestListener(true, isThumbnailRequest = true)) + ) + } else { + it + } + } + // Request image from the network on fail load image from cache + .error( + glide.load(url) + .centerInside() + .addListener(ImageRequestListener(false, isThumbnailRequest = false)) + ) + .centerInside() + .addListener(ImageRequestListener(true, isThumbnailRequest = false)) + .into(photoView) + } + + /** + * We start transition as soon as we think reasonable but we must take care about couple of + * things> + * - Do not change image in the middle of transition. It messes up the view. + * - Do not transition for the views which don't require it. Starting transition from + * multiple fragments does weird things + * - Do not wait to transition until the image loads from network + * + * Preview, cached image, network image, x - failed, o - succeeded + * P C N - start transition after... + * x x x - the cache fails + * x x o - the cache fails + * x o o - the cache succeeds + * o x o - the preview succeeds. Do not start on cache. + * o o o - the preview succeeds. Do not start on cache. + * + * So start transition after the first success or after anything with the cache + * + * @param isCacheRequest - is this listener for request image from cache or from the network + */ + private inner class ImageRequestListener( + private val isCacheRequest: Boolean, + private val isThumbnailRequest: Boolean + ) : RequestListener<Drawable> { + + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target<Drawable>, + isFirstResource: Boolean + ): Boolean { + // If cache for full image failed complete transition + if (isCacheRequest && !isThumbnailRequest && shouldStartTransition && + !startedTransition + ) { + photoActionsListener.onBringUp() + } + // Hide progress bar only on fail request from internet + if (!isCacheRequest) binding.progressBar.hide() + // We don't want to overwrite preview with null when main image fails to load + return !isCacheRequest + } + + @SuppressLint("CheckResult") + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target<Drawable>, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + binding.progressBar.hide() // Always hide the progress bar on success + + if (!startedTransition || !shouldStartTransition) { + // Set this right away so that we don't have to concurrent post() requests + startedTransition = true + // post() because load() replaces image with null. Sometimes after we set + // the thumbnail. + binding.photoView.post { + target.onResourceReady(resource, null) + if (shouldStartTransition) photoActionsListener.onBringUp() + } + } else { + // This waits for transition. If there's no transition then we should hit + // another branch. When the view is destroyed the coroutine is automatically canceled. + transition?.let { + viewLifecycleOwner.lifecycleScope.launch { + it.await() + target.onResourceReady(resource, null) + } + } + } + return true + } + } + + override fun onTransitionEnd() { + this.transition?.complete(Unit) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt new file mode 100644 index 0000000..1e6f456 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewMediaFragment.kt @@ -0,0 +1,106 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.fragment + +import android.os.Bundle +import android.text.TextUtils +import androidx.fragment.app.Fragment +import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.entity.Attachment + +abstract class ViewMediaFragment : Fragment() { + private var toolbarVisibilityDisposable: Function0<Boolean>? = null + + abstract fun setupMediaView( + url: String, + previewUrl: String?, + description: String?, + showingDescription: Boolean + ) + + abstract fun onToolbarVisibilityChange(visible: Boolean) + + protected var showingDescription = false + protected var isDescriptionVisible = false + + companion object { + @JvmStatic + protected val ARG_START_POSTPONED_TRANSITION = "startPostponedTransition" + + @JvmStatic + protected val ARG_ATTACHMENT = "attach" + + @JvmStatic + protected val ARG_SINGLE_IMAGE_URL = "singleImageUrl" + + @JvmStatic + fun newInstance( + attachment: Attachment, + shouldStartPostponedTransition: Boolean + ): ViewMediaFragment { + val arguments = Bundle(2) + arguments.putParcelable(ARG_ATTACHMENT, attachment) + arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, shouldStartPostponedTransition) + + val fragment = when (attachment.type) { + Attachment.Type.IMAGE -> ViewImageFragment() + Attachment.Type.VIDEO, + Attachment.Type.GIFV, + Attachment.Type.AUDIO -> ViewVideoFragment() + else -> ViewImageFragment() // it probably won't show anything, but its better than crashing + } + fragment.arguments = arguments + return fragment + } + + @JvmStatic + fun newSingleImageInstance(imageUrl: String): ViewMediaFragment { + val arguments = Bundle(2) + val fragment = ViewImageFragment() + arguments.putString(ARG_SINGLE_IMAGE_URL, imageUrl) + arguments.putBoolean(ARG_START_POSTPONED_TRANSITION, true) + + fragment.arguments = arguments + return fragment + } + } + + abstract fun onTransitionEnd() + + protected fun finalizeViewSetup(url: String, previewUrl: String?, description: String?) { + val mediaActivity = activity as ViewMediaActivity + + showingDescription = !TextUtils.isEmpty(description) + isDescriptionVisible = showingDescription + setupMediaView( + url, + previewUrl, + description, + showingDescription && mediaActivity.isToolbarVisible + ) + + toolbarVisibilityDisposable = (activity as ViewMediaActivity) + .addToolbarVisibilityListener { isVisible -> + onToolbarVisibilityChange(isVisible) + } + } + + override fun onDestroyView() { + toolbarVisibilityDisposable?.invoke() + toolbarVisibilityDisposable = null + super.onDestroyView() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt new file mode 100644 index 0000000..6752562 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt @@ -0,0 +1,421 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.fragment + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.annotation.SuppressLint +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.method.ScrollingMovementMethod +import android.view.GestureDetector +import android.view.Gravity +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.LinearLayout +import androidx.annotation.OptIn +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.media3.common.AudioAttributes +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.PlaybackException +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.util.EventLogger +import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.PlayerView +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.CustomViewTarget +import com.bumptech.glide.request.transition.Transition +import com.google.android.material.snackbar.Snackbar +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.ViewMediaActivity +import com.keylesspalace.tusky.databinding.FragmentViewVideoBinding +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.util.getParcelableCompat +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.unsafeLazy +import com.keylesspalace.tusky.util.viewBinding +import com.keylesspalace.tusky.util.visible +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import javax.inject.Provider +import kotlin.math.abs + +@AndroidEntryPoint +@OptIn(UnstableApi::class) +class ViewVideoFragment : ViewMediaFragment() { + interface VideoActionsListener { + fun onDismiss() + } + + @Inject + lateinit var playerProvider: Provider<ExoPlayer> + + private val binding by viewBinding(FragmentViewVideoBinding::bind) + + private val videoActionsListener: VideoActionsListener + get() = requireActivity() as VideoActionsListener + private val handler = Handler(Looper.getMainLooper()) + private val hideToolbar = Runnable { + // Hoist toolbar hiding to activity so it can track state across different fragments + // This is explicitly stored as runnable so that we pass it to the handler later for cancellation + mediaActivity.onPhotoTap() + } + private val mediaActivity: ViewMediaActivity + get() = requireActivity() as ViewMediaActivity + private val isAudio + get() = mediaAttachment.type == Attachment.Type.AUDIO + + private val mediaAttachment: Attachment by unsafeLazy { + arguments?.getParcelableCompat<Attachment>(ARG_ATTACHMENT) + ?: throw IllegalArgumentException("attachment has to be set") + } + + private var player: ExoPlayer? = null + + /** The saved seek position, if the fragment is being resumed */ + private var savedSeekPosition: Long = 0 + + /** Have we received at least one "READY" event? */ + private var haveStarted = false + + /** Is there a pending autohide? (We can't rely on Android's tracking because that clears on suspend.) */ + private var pendingHideToolbar = false + + /** Prevent the next play start from queueing a toolbar hide. */ + private var suppressNextHideToolbar = false + + @SuppressLint("PrivateResource", "MissingInflatedId") + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val rootView = inflater.inflate(R.layout.fragment_view_video, container, false) + + // Move the controls to the bottom of the screen, with enough bottom margin to clear the seekbar + val controls = rootView.findViewById<LinearLayout>( + androidx.media3.ui.R.id.exo_center_controls + ) + val layoutParams = controls.layoutParams as FrameLayout.LayoutParams + layoutParams.gravity = Gravity.CENTER_HORIZONTAL or Gravity.BOTTOM + layoutParams.bottomMargin = rootView.context.resources.getDimension(androidx.media3.ui.R.dimen.exo_styled_bottom_bar_height) + .toInt() + controls.layoutParams = layoutParams + + return rootView + } + + @SuppressLint("ClickableViewAccessibility") + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + /** + * Handle single taps, flings, and dragging + */ + val touchListener = object : View.OnTouchListener { + var lastY = 0f + + /** The view that contains the playing content */ + // binding.videoView is fullscreen, and includes the controls, so don't use that + // when scaling in response to the user dragging on the screen + val contentFrame = binding.videoView.findViewById<AspectRatioFrameLayout>( + androidx.media3.ui.R.id.exo_content_frame + ) + + /** Handle taps and flings */ + val simpleGestureDetector = GestureDetector( + requireContext(), + object : GestureDetector.SimpleOnGestureListener() { + override fun onDown(e: MotionEvent) = true + + /** A single tap should show/hide the media description */ + override fun onSingleTapUp(e: MotionEvent): Boolean { + mediaActivity.onPhotoTap() + return true // Do not pass gestures through to media3 + } + + /** A fling up/down should dismiss the fragment */ + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean { + if (abs(velocityY) > abs(velocityX)) { + videoActionsListener.onDismiss() + return true + } + return true // Do not pass gestures through to media3 + } + } + ) + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View?, event: MotionEvent): Boolean { + // Track movement, and scale / translate the video display accordingly + if (event.action == MotionEvent.ACTION_DOWN) { + lastY = event.rawY + } else if (event.pointerCount == 1 && event.action == MotionEvent.ACTION_MOVE) { + val diff = event.rawY - lastY + if (contentFrame.translationY != 0f || abs(diff) > 40) { + contentFrame.translationY += diff + val scale = (-abs(contentFrame.translationY) / 720 + 1).coerceAtLeast(0.5f) + contentFrame.scaleY = scale + contentFrame.scaleX = scale + lastY = event.rawY + } + } else if (event.action == MotionEvent.ACTION_UP || event.action == MotionEvent.ACTION_CANCEL) { + if (abs(contentFrame.translationY) > 180) { + videoActionsListener.onDismiss() + } else { + contentFrame.animate().translationY(0f).scaleX(1f).scaleY(1f).start() + } + } + + simpleGestureDetector.onTouchEvent(event) + + // Do not pass gestures through to media3 + // We have to do this because otherwise taps to hide will be double-handled and media3 will re-show itself + // media3 has a property to disable "hide on tap" but "show on tap" is unconditional + return true + } + } + + val mediaPlayerListener = object : Player.Listener { + @SuppressLint("ClickableViewAccessibility", "SyntheticAccessor") + @OptIn(UnstableApi::class) + override fun onPlaybackStateChanged(playbackState: Int) { + when (playbackState) { + Player.STATE_READY -> { + if (!haveStarted) { + // Wait until the media is loaded before accepting taps as we don't want toolbar to + // be hidden until then. + binding.videoView.setOnTouchListener(touchListener) + + binding.progressBar.hide() + binding.videoView.useController = true + binding.videoView.showController() + haveStarted = true + } else { + // This isn't a real "done loading"; this is a resume event after backgrounding. + if (mediaActivity.isToolbarVisible) { + // Before suspend, the toolbar/description were visible, so description is visible already. + // But media3 will have automatically hidden the video controls on suspend, so we need to match the description state. + binding.videoView.showController() + if (!pendingHideToolbar) { + suppressNextHideToolbar = true // The user most recently asked us to show the toolbar, so don't hide it when play starts. + } + } else { + mediaActivity.onPhotoTap() + } + } + } + else -> { /* do nothing */ } + } + } + + override fun onIsPlayingChanged(isPlaying: Boolean) { + if (isAudio) return + if (isPlaying) { + if (suppressNextHideToolbar) { + suppressNextHideToolbar = false + } else { + hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) + } + } else { + handler.removeCallbacks(hideToolbar) + } + } + + @SuppressLint("SyntheticAccessor") + override fun onPlayerError(error: PlaybackException) { + binding.progressBar.hide() + val message = getString( + R.string.error_media_playback, + error.cause?.message ?: error.message + ) + Snackbar.make(binding.root, message, Snackbar.LENGTH_INDEFINITE) + .setTextMaxLines(10) + .setAction(R.string.action_retry) { player?.prepare() } + .show() + } + } + + savedSeekPosition = savedInstanceState?.getLong(SEEK_POSITION) ?: 0 + + val attachment = mediaAttachment + finalizeViewSetup(attachment.url, attachment.previewUrl, attachment.description) + + // Lifecycle callbacks + viewLifecycleOwner.lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + initializePlayer(mediaPlayerListener) + binding.videoView.onResume() + } + + override fun onStop(owner: LifecycleOwner) { + // This might be multi-window, so pause everything now. + binding.videoView.onPause() + releasePlayer() + handler.removeCallbacks(hideToolbar) + } + }) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putLong(SEEK_POSITION, savedSeekPosition) + } + + private fun initializePlayer(mediaPlayerListener: Player.Listener) { + player = playerProvider.get().apply { + setAudioAttributes( + AudioAttributes.Builder() + .setContentType(if (isAudio) C.AUDIO_CONTENT_TYPE_UNKNOWN else C.AUDIO_CONTENT_TYPE_MOVIE) + .setUsage(C.USAGE_MEDIA) + .build(), + true + ) + if (BuildConfig.DEBUG) addAnalyticsListener(EventLogger("$TAG:ExoPlayer")) + setMediaItem(MediaItem.fromUri(mediaAttachment.url)) + addListener(mediaPlayerListener) + repeatMode = Player.REPEAT_MODE_ONE + playWhenReady = true + seekTo(savedSeekPosition) + prepare() + } + + binding.videoView.player = player + + // Audio-only files might have a preview image. If they do, set it as the artwork + if (isAudio) { + mediaAttachment.previewUrl?.let { url -> + Glide.with(this) + .load(url) + .into( + object : CustomViewTarget<PlayerView, Drawable>(binding.videoView) { + override fun onLoadFailed(errorDrawable: Drawable?) { + // Don't do anything + } + + override fun onResourceCleared(placeholder: Drawable?) { + view.defaultArtwork = null + } + + override fun onResourceReady( + resource: Drawable, + transition: Transition<in Drawable>? + ) { + view.defaultArtwork = resource + } + }.clearOnDetach() + ) + } + } + } + + private fun releasePlayer() { + player?.let { + savedSeekPosition = it.currentPosition + it.release() + player = null + binding.videoView.player = null + } + } + + @SuppressLint("ClickableViewAccessibility") + override fun setupMediaView( + url: String, + previewUrl: String?, + description: String?, + showingDescription: Boolean + ) { + binding.mediaDescription.text = description + binding.mediaDescription.visible(showingDescription) + binding.mediaDescription.movementMethod = ScrollingMovementMethod() + + // Ensure the description is visible over the video + binding.mediaDescription.elevation = binding.videoView.elevation + 1 + + binding.videoView.transitionName = url + + binding.videoView.requestFocus() + + if (requireArguments().getBoolean(ARG_START_POSTPONED_TRANSITION)) { + mediaActivity.onBringUp() + } + } + + private fun hideToolbarAfterDelay(delayMilliseconds: Int) { + pendingHideToolbar = true + handler.postDelayed(hideToolbar, delayMilliseconds.toLong()) + } + + override fun onToolbarVisibilityChange(visible: Boolean) { + if (view == null) { + return + } + + isDescriptionVisible = showingDescription && visible + val alpha = if (isDescriptionVisible) 1.0f else 0.0f + if (isDescriptionVisible) { + // If to be visible, need to make visible immediately and animate alpha + binding.mediaDescription.alpha = 0.0f + binding.mediaDescription.visible(isDescriptionVisible) + } + + binding.mediaDescription.animate().alpha(alpha) + .setListener(object : AnimatorListenerAdapter() { + @SuppressLint("SyntheticAccessor") + override fun onAnimationEnd(animation: Animator) { + view ?: return + binding.mediaDescription.visible(isDescriptionVisible) + animation.removeListener(this) + } + }) + .start() + + // media3 controls bar + if (visible) { + binding.videoView.showController() + } else { + binding.videoView.hideController() + } + + // Either the user just requested toolbar display, or we just hid it. + // Either way, any pending hides are no longer appropriate. + pendingHideToolbar = false + handler.removeCallbacks(hideToolbar) + } + + override fun onTransitionEnd() { } + + companion object { + private const val TAG = "ViewVideoFragment" + private const val TOOLBAR_HIDE_DELAY_MS = 4_000 + private const val SEEK_POSITION = "seekPosition" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountActionListener.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountActionListener.kt new file mode 100644 index 0000000..654b50d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountActionListener.kt @@ -0,0 +1,22 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ +package com.keylesspalace.tusky.interfaces + +interface AccountActionListener { + fun onViewAccount(id: String) + fun onMute(mute: Boolean, id: String, position: Int, notifications: Boolean) + fun onBlock(block: Boolean, id: String, position: Int) + fun onRespondToFollowRequest(accept: Boolean, id: String, position: Int) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountSelectionListener.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountSelectionListener.kt new file mode 100644 index 0000000..272a16a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/AccountSelectionListener.kt @@ -0,0 +1,22 @@ +/* Copyright 2019 Levi Bard + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.interfaces + +import com.keylesspalace.tusky.db.entity.AccountEntity + +interface AccountSelectionListener { + fun onAccountSelected(account: AccountEntity) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/ActionButtonActivity.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/ActionButtonActivity.java new file mode 100644 index 0000000..089331a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/ActionButtonActivity.java @@ -0,0 +1,26 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.interfaces; + +import androidx.annotation.Nullable; +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +public interface ActionButtonActivity { + + /* return the ActionButton of the Activity to hide or show it on scroll */ + @Nullable + FloatingActionButton getActionButton(); +} diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/FabFragment.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/FabFragment.kt new file mode 100644 index 0000000..1189dd3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/FabFragment.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.interfaces + +interface FabFragment { + fun isFabVisible(): Boolean +} diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/HashtagActionListener.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/HashtagActionListener.kt new file mode 100644 index 0000000..e617195 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/HashtagActionListener.kt @@ -0,0 +1,7 @@ +package com.keylesspalace.tusky.interfaces + +interface HashtagActionListener { + fun unfollow(tagName: String, position: Int) + fun viewTag(tagName: String) + fun copyTagName(tagName: String) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.kt new file mode 100644 index 0000000..56faefa --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/LinkListener.kt @@ -0,0 +1,22 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.interfaces + +interface LinkListener { + fun onViewTag(tag: String) + fun onViewAccount(id: String) + fun onViewUrl(url: String) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/RefreshableFragment.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/RefreshableFragment.kt new file mode 100644 index 0000000..83fc20c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/RefreshableFragment.kt @@ -0,0 +1,11 @@ +package com.keylesspalace.tusky.interfaces + +/** + * Created by pandasoft (joelpyska1@gmail.com) on 04/04/2019. + */ +interface RefreshableFragment { + /** + * Call this method to refresh fragment content + */ + fun refreshContent() +} diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/ReselectableFragment.kt b/app/src/main/java/com/keylesspalace/tusky/interfaces/ReselectableFragment.kt new file mode 100644 index 0000000..598894f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/ReselectableFragment.kt @@ -0,0 +1,11 @@ +package com.keylesspalace.tusky.interfaces + +/** + * Created by pandasoft (joelpyska1@gmail.com) on 04/04/2019. + */ +interface ReselectableFragment { + /** + * Call this method when tab reselected + */ + fun onReselect() +} diff --git a/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java new file mode 100644 index 0000000..75f6ed7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/interfaces/StatusActionListener.java @@ -0,0 +1,71 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.interfaces; + +import android.view.View; + +import java.util.List; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public interface StatusActionListener extends LinkListener { + void onReply(int position); + void onReblog(final boolean reblog, final int position); + void onFavourite(final boolean favourite, final int position); + void onBookmark(final boolean bookmark, final int position); + void onMore(@NonNull View view, final int position); + void onViewMedia(int position, int attachmentIndex, @Nullable View view); + void onViewThread(int position); + + /** + * Open reblog author for the status. + * @param position At which position in the list status is located + */ + void onOpenReblog(int position); + void onExpandedChange(boolean expanded, int position); + void onContentHiddenChange(boolean isShowing, int position); + void onLoadMore(int position); + + /** + * Called when the status {@link android.widget.ToggleButton} responsible for collapsing long + * status content is interacted with. + * + * @param isCollapsed Whether the status content is shown in a collapsed state or fully. + * @param position The position of the status in the list. + */ + void onContentCollapsedChange(boolean isCollapsed, int position); + + /** + * called when the reblog count has been clicked + * @param position The position of the status in the list. + */ + default void onShowReblogs(int position) {} + + /** + * called when the favourite count has been clicked + * @param position The position of the status in the list. + */ + default void onShowFavs(int position) {} + + void onVoteInPoll(int position, @NonNull List<Integer> choices); + + default void onShowEdits(int position) {} + + void clearWarningAction(int position); + + void onUntranslate(int position); +} diff --git a/app/src/main/java/com/keylesspalace/tusky/json/Guarded.kt b/app/src/main/java/com/keylesspalace/tusky/json/Guarded.kt new file mode 100644 index 0000000..3d37b38 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/json/Guarded.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.json + +import com.squareup.moshi.JsonQualifier + +@Retention(AnnotationRetention.RUNTIME) +@JsonQualifier +internal annotation class Guarded diff --git a/app/src/main/java/com/keylesspalace/tusky/json/GuardedAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/json/GuardedAdapter.kt new file mode 100644 index 0000000..11cb1f3 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/json/GuardedAdapter.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.json + +import com.squareup.moshi.JsonAdapter +import com.squareup.moshi.JsonDataException +import com.squareup.moshi.JsonReader +import com.squareup.moshi.JsonWriter +import com.squareup.moshi.Moshi +import com.squareup.moshi.Types +import java.lang.reflect.Type + +/** + * This adapter tries to parse the value using a delegated parser + * and returns null in case of error. + */ +class GuardedAdapter<T> private constructor( + private val delegate: JsonAdapter<T> +) : JsonAdapter<T>() { + + override fun fromJson(reader: JsonReader): T? { + return try { + delegate.fromJson(reader) + } catch (e: JsonDataException) { + reader.skipValue() + null + } + } + + override fun toJson(writer: JsonWriter, value: T?) { + delegate.toJson(writer, value) + } + + companion object { + val ANNOTATION_FACTORY = object : Factory { + override fun create( + type: Type, + annotations: Set<Annotation>, + moshi: Moshi + ): JsonAdapter<*>? { + val delegateAnnotations = + Types.nextAnnotations(annotations, Guarded::class.java) ?: return null + val delegate = moshi.nextAdapter<Any?>(this, type, delegateAnnotations) + return GuardedAdapter(delegate) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt b/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt new file mode 100644 index 0000000..3763fa5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/network/FilterModel.kt @@ -0,0 +1,86 @@ +package com.keylesspalace.tusky.network + +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.FilterV1 +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.util.parseAsMastodonHtml +import java.util.Date +import java.util.regex.Pattern +import javax.inject.Inject + +/** + * One-stop for status filtering logic using Mastodon's filters. + * + * 1. You init with [initWithFilters], this compiles regex pattern. + * 2. You call [shouldFilterStatus] to figure out what to display when you load statuses. + */ +class FilterModel @Inject constructor() { + private var pattern: Pattern? = null + private var v1 = false + lateinit var kind: Filter.Kind + + fun initWithFilters(filters: List<FilterV1>) { + v1 = true + this.pattern = makeFilter(filters) + } + + fun shouldFilterStatus(status: Status): Filter.Action { + if (v1) { + // Patterns are expensive and thread-safe, matchers are neither. + val matcher = pattern?.matcher("") ?: return Filter.Action.NONE + + if (status.poll?.options?.any { matcher.reset(it.title).find() } == true) { + return Filter.Action.HIDE + } + + val spoilerText = status.actionableStatus.spoilerText + val attachmentsDescriptions = status.attachments.mapNotNull { it.description } + + return if ( + matcher.reset(status.actionableStatus.content.parseAsMastodonHtml().toString()).find() || + (spoilerText.isNotEmpty() && matcher.reset(spoilerText).find()) || + (attachmentsDescriptions.isNotEmpty() && matcher.reset(attachmentsDescriptions.joinToString("\n")).find()) + ) { + Filter.Action.HIDE + } else { + Filter.Action.NONE + } + } + + val matchingKind = status.filtered.orEmpty().filter { result -> + result.filter.kinds.contains(kind) + } + + return if (matchingKind.isEmpty()) { + Filter.Action.NONE + } else { + matchingKind.maxOf { it.filter.action } + } + } + + private fun filterToRegexToken(filter: FilterV1): String? { + val phrase = filter.phrase + val quotedPhrase = Pattern.quote(phrase) + return if (filter.wholeWord && ALPHANUMERIC.matcher(phrase).matches()) { + String.format("(^|\\W)%s($|\\W)", quotedPhrase) + } else { + quotedPhrase + } + } + + private fun makeFilter(filters: List<FilterV1>): Pattern? { + val now = Date() + val nonExpiredFilters = filters.filter { it.expiresAt?.before(now) != true } + if (nonExpiredFilters.isEmpty()) return null + val tokens = nonExpiredFilters + .asSequence() + .map { filterToRegexToken(it) } + .joinToString("|") + + return Pattern.compile(tokens, Pattern.CASE_INSENSITIVE) + } + + companion object { + private val ALPHANUMERIC = Pattern.compile("^\\w+$") + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.kt b/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.kt new file mode 100644 index 0000000..6aabaa1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptor.kt @@ -0,0 +1,84 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.network + +import android.util.Log +import com.keylesspalace.tusky.db.AccountManager +import java.io.IOException +import okhttp3.HttpUrl +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Protocol +import okhttp3.Request +import okhttp3.Response +import okhttp3.ResponseBody.Companion.toResponseBody + +class InstanceSwitchAuthInterceptor(private val accountManager: AccountManager) : Interceptor { + + @Throws(IOException::class) + override fun intercept(chain: Interceptor.Chain): Response { + val originalRequest: Request = chain.request() + + // only switch domains if the request comes from retrofit + return if (originalRequest.url.host == MastodonApi.PLACEHOLDER_DOMAIN) { + val builder: Request.Builder = originalRequest.newBuilder() + val instanceHeader = originalRequest.header(MastodonApi.DOMAIN_HEADER) + + if (instanceHeader != null) { + // use domain explicitly specified in custom header + builder.url(swapHost(originalRequest.url, instanceHeader)) + builder.removeHeader(MastodonApi.DOMAIN_HEADER) + } else { + val currentAccount = accountManager.activeAccount + + if (currentAccount != null) { + val accessToken = currentAccount.accessToken + if (accessToken.isNotEmpty()) { + // use domain of current account + builder.url(swapHost(originalRequest.url, currentAccount.domain)) + .header("Authorization", "Bearer %s".format(accessToken)) + } + } + } + + val newRequest: Request = builder.build() + + if (MastodonApi.PLACEHOLDER_DOMAIN == newRequest.url.host) { + Log.w( + "ISAInterceptor", + "no user logged in or no domain header specified - can't make request to " + newRequest.url + ) + return Response.Builder() + .code(400) + .message("Bad Request") + .protocol(Protocol.HTTP_2) + .body("".toResponseBody("text/plain".toMediaType())) + .request(chain.request()) + .build() + } + + chain.proceed(newRequest) + } else { + chain.proceed(originalRequest) + } + } + + companion object { + private fun swapHost(url: HttpUrl, host: String): HttpUrl { + return url.newBuilder().host(host).build() + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt new file mode 100644 index 0000000..9ad49f5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/network/MastodonApi.kt @@ -0,0 +1,725 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.network + +import at.connyduck.calladapter.networkresult.NetworkResult +import com.keylesspalace.tusky.entity.AccessToken +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Announcement +import com.keylesspalace.tusky.entity.AppCredentials +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Conversation +import com.keylesspalace.tusky.entity.DeletedStatus +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.FilterKeyword +import com.keylesspalace.tusky.entity.FilterV1 +import com.keylesspalace.tusky.entity.HashTag +import com.keylesspalace.tusky.entity.Instance +import com.keylesspalace.tusky.entity.InstanceV1 +import com.keylesspalace.tusky.entity.Marker +import com.keylesspalace.tusky.entity.MastoList +import com.keylesspalace.tusky.entity.MediaUploadResult +import com.keylesspalace.tusky.entity.NewStatus +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.entity.NotificationSubscribeResult +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.Relationship +import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.entity.SearchResult +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.StatusContext +import com.keylesspalace.tusky.entity.StatusEdit +import com.keylesspalace.tusky.entity.StatusSource +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.entity.Translation +import com.keylesspalace.tusky.entity.TrendingTag +import okhttp3.MultipartBody +import okhttp3.RequestBody +import retrofit2.Response +import retrofit2.http.Body +import retrofit2.http.DELETE +import retrofit2.http.Field +import retrofit2.http.FieldMap +import retrofit2.http.FormUrlEncoded +import retrofit2.http.GET +import retrofit2.http.HTTP +import retrofit2.http.Header +import retrofit2.http.Multipart +import retrofit2.http.PATCH +import retrofit2.http.POST +import retrofit2.http.PUT +import retrofit2.http.Part +import retrofit2.http.Path +import retrofit2.http.Query + +/** + * for documentation of the Mastodon REST API see https://docs.joinmastodon.org/api/ + */ + +@JvmSuppressWildcards +interface MastodonApi { + + companion object { + const val ENDPOINT_AUTHORIZE = "oauth/authorize" + const val DOMAIN_HEADER = "domain" + const val PLACEHOLDER_DOMAIN = "dummy.placeholder" + } + + @GET("/api/v1/custom_emojis") + suspend fun getCustomEmojis(): NetworkResult<List<Emoji>> + + @GET("api/v1/instance") + suspend fun getInstanceV1( + @Header(DOMAIN_HEADER) domain: String? = null + ): NetworkResult<InstanceV1> + + @GET("api/v2/instance") + suspend fun getInstance( + @Header(DOMAIN_HEADER) domain: String? = null + ): NetworkResult<Instance> + + @GET("api/v1/filters") + suspend fun getFiltersV1(): NetworkResult<List<FilterV1>> + + @GET("api/v2/filters/{filterId}") + suspend fun getFilter(@Path("filterId") filterId: String): NetworkResult<Filter> + + @GET("api/v2/filters") + suspend fun getFilters(): NetworkResult<List<Filter>> + + @GET("api/v1/timelines/home") + @Throws(Exception::class) + suspend fun homeTimeline( + @Query("max_id") maxId: String? = null, + @Query("min_id") minId: String? = null, + @Query("since_id") sinceId: String? = null, + @Query("limit") limit: Int? = null + ): Response<List<Status>> + + @GET("api/v1/timelines/public") + suspend fun publicTimeline( + @Query("local") local: Boolean? = null, + @Query("max_id") maxId: String? = null, + @Query("since_id") sinceId: String? = null, + @Query("limit") limit: Int? = null + ): Response<List<Status>> + + @GET("api/v1/timelines/tag/{hashtag}") + suspend fun hashtagTimeline( + @Path("hashtag") hashtag: String, + @Query("any[]") any: List<String>?, + @Query("local") local: Boolean?, + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? + ): Response<List<Status>> + + @GET("api/v1/timelines/list/{listId}") + suspend fun listTimeline( + @Path("listId") listId: String, + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? + ): Response<List<Status>> + + @GET("api/v1/notifications") + @Throws(Exception::class) + suspend fun notifications( + /** Return results older than this ID */ + @Query("max_id") maxId: String? = null, + /** Return results newer than this ID */ + @Query("since_id") sinceId: String? = null, + /** Return results immediately newer than this ID */ + @Query("min_id") minId: String? = null, + /** Maximum number of results to return. Defaults to 15, max is 30 */ + @Query("limit") limit: Int? = null, + /** Types to excludes from the results */ + @Query("exclude_types[]") excludes: Set<Notification.Type>? = null + ): Response<List<Notification>> + + /** Fetch a single notification */ + @GET("api/v1/notifications/{id}") + suspend fun notification(@Path("id") id: String): Response<Notification> + + @GET("api/v1/markers") + suspend fun markersWithAuth( + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Query("timeline[]") timelines: List<String> + ): Map<String, Marker> + + @FormUrlEncoded + @POST("api/v1/markers") + suspend fun updateMarkersWithAuth( + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Field("home[last_read_id]") homeLastReadId: String? = null, + @Field("notifications[last_read_id]") notificationsLastReadId: String? = null + ): NetworkResult<Unit> + + @GET("api/v1/notifications") + suspend fun notificationsWithAuth( + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + /** Return results immediately newer than this ID */ + @Query("min_id") minId: String? + ): Response<List<Notification>> + + @POST("api/v1/notifications/clear") + suspend fun clearNotifications(): NetworkResult<Unit> + + @FormUrlEncoded + @PUT("api/v1/media/{mediaId}") + suspend fun updateMedia( + @Path("mediaId") mediaId: String, + @Field("description") description: String?, + @Field("focus") focus: String? + ): NetworkResult<Attachment> + + @GET("api/v1/media/{mediaId}") + suspend fun getMedia(@Path("mediaId") mediaId: String): Response<MediaUploadResult> + + @POST("api/v1/statuses") + suspend fun createStatus( + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Header("Idempotency-Key") idempotencyKey: String, + @Body status: NewStatus + ): NetworkResult<Status> + + @POST("api/v1/statuses") + suspend fun createScheduledStatus( + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Header("Idempotency-Key") idempotencyKey: String, + @Body status: NewStatus + ): NetworkResult<ScheduledStatus> + + @GET("api/v1/statuses/{id}") + suspend fun status(@Path("id") statusId: String): NetworkResult<Status> + + @PUT("api/v1/statuses/{id}") + suspend fun editStatus( + @Path("id") statusId: String, + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Header("Idempotency-Key") idempotencyKey: String, + @Body editedStatus: NewStatus + ): NetworkResult<Status> + + @GET("api/v1/statuses/{id}/source") + suspend fun statusSource(@Path("id") statusId: String): NetworkResult<StatusSource> + + @GET("api/v1/statuses/{id}/context") + suspend fun statusContext(@Path("id") statusId: String): NetworkResult<StatusContext> + + @GET("api/v1/statuses/{id}/history") + suspend fun statusEdits(@Path("id") statusId: String): NetworkResult<List<StatusEdit>> + + @GET("api/v1/statuses/{id}/reblogged_by") + suspend fun statusRebloggedBy( + @Path("id") statusId: String, + @Query("max_id") maxId: String? + ): Response<List<TimelineAccount>> + + @GET("api/v1/statuses/{id}/favourited_by") + suspend fun statusFavouritedBy( + @Path("id") statusId: String, + @Query("max_id") maxId: String? + ): Response<List<TimelineAccount>> + + @DELETE("api/v1/statuses/{id}") + suspend fun deleteStatus(@Path("id") statusId: String): NetworkResult<DeletedStatus> + + @POST("api/v1/statuses/{id}/reblog") + suspend fun reblogStatus(@Path("id") statusId: String): NetworkResult<Status> + + @POST("api/v1/statuses/{id}/unreblog") + suspend fun unreblogStatus(@Path("id") statusId: String): NetworkResult<Status> + + @POST("api/v1/statuses/{id}/favourite") + suspend fun favouriteStatus(@Path("id") statusId: String): NetworkResult<Status> + + @POST("api/v1/statuses/{id}/unfavourite") + suspend fun unfavouriteStatus(@Path("id") statusId: String): NetworkResult<Status> + + @POST("api/v1/statuses/{id}/bookmark") + suspend fun bookmarkStatus(@Path("id") statusId: String): NetworkResult<Status> + + @POST("api/v1/statuses/{id}/unbookmark") + suspend fun unbookmarkStatus(@Path("id") statusId: String): NetworkResult<Status> + + @POST("api/v1/statuses/{id}/pin") + suspend fun pinStatus(@Path("id") statusId: String): NetworkResult<Status> + + @POST("api/v1/statuses/{id}/unpin") + suspend fun unpinStatus(@Path("id") statusId: String): NetworkResult<Status> + + @POST("api/v1/statuses/{id}/mute") + suspend fun muteConversation(@Path("id") statusId: String): NetworkResult<Status> + + @POST("api/v1/statuses/{id}/unmute") + suspend fun unmuteConversation(@Path("id") statusId: String): NetworkResult<Status> + + @GET("api/v1/scheduled_statuses") + suspend fun scheduledStatuses( + @Query("limit") limit: Int? = null, + @Query("max_id") maxId: String? = null + ): NetworkResult<List<ScheduledStatus>> + + @DELETE("api/v1/scheduled_statuses/{id}") + suspend fun deleteScheduledStatus( + @Path("id") scheduledStatusId: String + ): NetworkResult<Unit> + + @GET("api/v1/accounts/verify_credentials") + suspend fun accountVerifyCredentials( + @Header(DOMAIN_HEADER) domain: String? = null, + @Header("Authorization") auth: String? = null + ): NetworkResult<Account> + + @FormUrlEncoded + @PATCH("api/v1/accounts/update_credentials") + suspend fun accountUpdateSource( + @Field("source[privacy]") privacy: String?, + @Field("source[sensitive]") sensitive: Boolean?, + @Field("source[language]") language: String? + ): NetworkResult<Account> + + @Multipart + @PATCH("api/v1/accounts/update_credentials") + suspend fun accountUpdateCredentials( + @Part(value = "display_name") displayName: RequestBody?, + @Part(value = "note") note: RequestBody?, + @Part(value = "locked") locked: RequestBody?, + @Part avatar: MultipartBody.Part?, + @Part header: MultipartBody.Part?, + @Part(value = "fields_attributes[0][name]") fieldName0: RequestBody?, + @Part(value = "fields_attributes[0][value]") fieldValue0: RequestBody?, + @Part(value = "fields_attributes[1][name]") fieldName1: RequestBody?, + @Part(value = "fields_attributes[1][value]") fieldValue1: RequestBody?, + @Part(value = "fields_attributes[2][name]") fieldName2: RequestBody?, + @Part(value = "fields_attributes[2][value]") fieldValue2: RequestBody?, + @Part(value = "fields_attributes[3][name]") fieldName3: RequestBody?, + @Part(value = "fields_attributes[3][value]") fieldValue3: RequestBody? + ): NetworkResult<Account> + + @GET("api/v1/accounts/search") + suspend fun searchAccounts( + @Query("q") query: String, + @Query("resolve") resolve: Boolean? = null, + @Query("limit") limit: Int? = null, + @Query("following") following: Boolean? = null + ): NetworkResult<List<TimelineAccount>> + + @GET("api/v1/accounts/{id}") + suspend fun account(@Path("id") accountId: String): NetworkResult<Account> + + /** + * Method to fetch statuses for the specified account. + * @param accountId ID for account for which statuses will be requested + * @param maxId Only statuses with ID less than maxID will be returned + * @param sinceId Only statuses with ID bigger than sinceID will be returned + * @param limit Limit returned statuses (current API limits: default - 20, max - 40) + * @param excludeReplies only return statuses that are no replies + * @param onlyMedia only return statuses that have media attached + */ + @GET("api/v1/accounts/{id}/statuses") + suspend fun accountStatuses( + @Path("id") accountId: String, + @Query("max_id") maxId: String? = null, + @Query("since_id") sinceId: String? = null, + @Query("limit") limit: Int? = null, + @Query("exclude_replies") excludeReplies: Boolean? = null, + @Query("only_media") onlyMedia: Boolean? = null, + @Query("pinned") pinned: Boolean? = null + ): Response<List<Status>> + + @GET("api/v1/accounts/{id}/followers") + suspend fun accountFollowers( + @Path("id") accountId: String, + @Query("max_id") maxId: String? + ): Response<List<TimelineAccount>> + + @GET("api/v1/accounts/{id}/following") + suspend fun accountFollowing( + @Path("id") accountId: String, + @Query("max_id") maxId: String? + ): Response<List<TimelineAccount>> + + @FormUrlEncoded + @POST("api/v1/accounts/{id}/follow") + suspend fun followAccount( + @Path("id") accountId: String, + @Field("reblogs") showReblogs: Boolean? = null, + @Field("notify") notify: Boolean? = null + ): NetworkResult<Relationship> + + @POST("api/v1/accounts/{id}/unfollow") + suspend fun unfollowAccount(@Path("id") accountId: String): NetworkResult<Relationship> + + @POST("api/v1/accounts/{id}/block") + suspend fun blockAccount(@Path("id") accountId: String): NetworkResult<Relationship> + + @POST("api/v1/accounts/{id}/unblock") + suspend fun unblockAccount(@Path("id") accountId: String): NetworkResult<Relationship> + + @FormUrlEncoded + @POST("api/v1/accounts/{id}/mute") + suspend fun muteAccount( + @Path("id") accountId: String, + @Field("notifications") notifications: Boolean? = null, + @Field("duration") duration: Int? = null + ): NetworkResult<Relationship> + + @POST("api/v1/accounts/{id}/unmute") + suspend fun unmuteAccount(@Path("id") accountId: String): NetworkResult<Relationship> + + @GET("api/v1/accounts/relationships") + suspend fun relationships( + @Query("id[]") accountIds: List<String> + ): NetworkResult<List<Relationship>> + + @POST("api/v1/pleroma/accounts/{id}/subscribe") + suspend fun subscribeAccount(@Path("id") accountId: String): NetworkResult<Relationship> + + @POST("api/v1/pleroma/accounts/{id}/unsubscribe") + suspend fun unsubscribeAccount(@Path("id") accountId: String): NetworkResult<Relationship> + + @GET("api/v1/blocks") + suspend fun blocks(@Query("max_id") maxId: String?): Response<List<TimelineAccount>> + + @GET("api/v1/mutes") + suspend fun mutes(@Query("max_id") maxId: String?): Response<List<TimelineAccount>> + + @GET("api/v1/domain_blocks") + suspend fun domainBlocks( + @Query("max_id") maxId: String? = null, + @Query("since_id") sinceId: String? = null, + @Query("limit") limit: Int? = null + ): Response<List<String>> + + @FormUrlEncoded + @POST("api/v1/domain_blocks") + suspend fun blockDomain(@Field("domain") domain: String): NetworkResult<Unit> + + @FormUrlEncoded + // @DELETE doesn't support fields + @HTTP(method = "DELETE", path = "api/v1/domain_blocks", hasBody = true) + suspend fun unblockDomain(@Field("domain") domain: String): NetworkResult<Unit> + + @GET("api/v1/favourites") + suspend fun favourites( + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? + ): Response<List<Status>> + + @GET("api/v1/bookmarks") + suspend fun bookmarks( + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("limit") limit: Int? + ): Response<List<Status>> + + @GET("api/v1/follow_requests") + suspend fun followRequests(@Query("max_id") maxId: String?): Response<List<TimelineAccount>> + + @POST("api/v1/follow_requests/{id}/authorize") + suspend fun authorizeFollowRequest(@Path("id") accountId: String): NetworkResult<Relationship> + + @POST("api/v1/follow_requests/{id}/reject") + suspend fun rejectFollowRequest(@Path("id") accountId: String): NetworkResult<Relationship> + + @FormUrlEncoded + @POST("api/v1/apps") + suspend fun authenticateApp( + @Header(DOMAIN_HEADER) domain: String, + @Field("client_name") clientName: String, + @Field("redirect_uris") redirectUris: String, + @Field("scopes") scopes: String, + @Field("website") website: String + ): NetworkResult<AppCredentials> + + @FormUrlEncoded + @POST("oauth/token") + suspend fun fetchOAuthToken( + @Header(DOMAIN_HEADER) domain: String, + @Field("client_id") clientId: String, + @Field("client_secret") clientSecret: String, + @Field("redirect_uri") redirectUri: String, + @Field("code") code: String, + @Field("grant_type") grantType: String + ): NetworkResult<AccessToken> + + @FormUrlEncoded + @POST("oauth/revoke") + suspend fun revokeOAuthToken( + @Field("client_id") clientId: String, + @Field("client_secret") clientSecret: String, + @Field("token") token: String + ): NetworkResult<Unit> + + @GET("/api/v1/lists") + suspend fun getLists(): NetworkResult<List<MastoList>> + + @GET("/api/v1/accounts/{id}/lists") + suspend fun getListsIncludesAccount( + @Path("id") accountId: String + ): NetworkResult<List<MastoList>> + + @FormUrlEncoded + @POST("api/v1/lists") + suspend fun createList( + @Field("title") title: String, + @Field("exclusive") exclusive: Boolean?, + @Field("replies_policy") replyPolicy: String + ): NetworkResult<MastoList> + + @FormUrlEncoded + @PUT("api/v1/lists/{listId}") + suspend fun updateList( + @Path("listId") listId: String, + @Field("title") title: String, + @Field("exclusive") exclusive: Boolean?, + @Field("replies_policy") replyPolicy: String + ): NetworkResult<MastoList> + + @DELETE("api/v1/lists/{listId}") + suspend fun deleteList(@Path("listId") listId: String): NetworkResult<Unit> + + @GET("api/v1/lists/{listId}/accounts") + suspend fun getAccountsInList( + @Path("listId") listId: String, + @Query("limit") limit: Int + ): NetworkResult<List<TimelineAccount>> + + @FormUrlEncoded + // @DELETE doesn't support fields + @HTTP(method = "DELETE", path = "api/v1/lists/{listId}/accounts", hasBody = true) + suspend fun deleteAccountFromList( + @Path("listId") listId: String, + @Field("account_ids[]") accountIds: List<String> + ): NetworkResult<Unit> + + @FormUrlEncoded + @POST("api/v1/lists/{listId}/accounts") + suspend fun addAccountToList( + @Path("listId") listId: String, + @Field("account_ids[]") accountIds: List<String> + ): NetworkResult<Unit> + + @GET("/api/v1/conversations") + suspend fun getConversations( + @Query("max_id") maxId: String? = null, + @Query("limit") limit: Int? = null + ): Response<List<Conversation>> + + @DELETE("/api/v1/conversations/{id}") + suspend fun deleteConversation(@Path("id") conversationId: String) + + @FormUrlEncoded + @POST("api/v1/filters") + suspend fun createFilterV1( + @Field("phrase") phrase: String, + @Field("context[]") context: List<String>, + @Field("irreversible") irreversible: Boolean?, + @Field("whole_word") wholeWord: Boolean?, + @Field("expires_in") expiresInSeconds: Int? + ): NetworkResult<FilterV1> + + @FormUrlEncoded + @PUT("api/v1/filters/{id}") + suspend fun updateFilterV1( + @Path("id") id: String, + @Field("phrase") phrase: String, + @Field("context[]") context: List<String>, + @Field("irreversible") irreversible: Boolean?, + @Field("whole_word") wholeWord: Boolean?, + @Field("expires_in") expiresInSeconds: Int? + ): NetworkResult<FilterV1> + + @DELETE("api/v1/filters/{id}") + suspend fun deleteFilterV1(@Path("id") id: String): NetworkResult<Unit> + + @FormUrlEncoded + @POST("api/v2/filters") + suspend fun createFilter( + @Field("title") title: String, + @Field("context[]") context: List<String>, + @Field("filter_action") filterAction: String, + @Field("expires_in") expiresInSeconds: Int? + ): NetworkResult<Filter> + + @FormUrlEncoded + @PUT("api/v2/filters/{id}") + suspend fun updateFilter( + @Path("id") id: String, + @Field("title") title: String? = null, + @Field("context[]") context: List<String>? = null, + @Field("filter_action") filterAction: String? = null, + @Field("expires_in") expiresInSeconds: Int? = null + ): NetworkResult<Filter> + + @DELETE("api/v2/filters/{id}") + suspend fun deleteFilter(@Path("id") id: String): NetworkResult<Unit> + + @FormUrlEncoded + @POST("api/v2/filters/{filterId}/keywords") + suspend fun addFilterKeyword( + @Path("filterId") filterId: String, + @Field("keyword") keyword: String, + @Field("whole_word") wholeWord: Boolean + ): NetworkResult<FilterKeyword> + + @FormUrlEncoded + @PUT("api/v2/filters/keywords/{keywordId}") + suspend fun updateFilterKeyword( + @Path("keywordId") keywordId: String, + @Field("keyword") keyword: String, + @Field("whole_word") wholeWord: Boolean + ): NetworkResult<FilterKeyword> + + @DELETE("api/v2/filters/keywords/{keywordId}") + suspend fun deleteFilterKeyword( + @Path("keywordId") keywordId: String + ): NetworkResult<Unit> + + @FormUrlEncoded + @POST("api/v1/polls/{id}/votes") + suspend fun voteInPoll( + @Path("id") id: String, + @Field("choices[]") choices: List<Int> + ): NetworkResult<Poll> + + @GET("api/v1/announcements") + suspend fun listAnnouncements( + @Query("with_dismissed") withDismissed: Boolean = true + ): NetworkResult<List<Announcement>> + + @POST("api/v1/announcements/{id}/dismiss") + suspend fun dismissAnnouncement(@Path("id") announcementId: String): NetworkResult<Unit> + + @PUT("api/v1/announcements/{id}/reactions/{name}") + suspend fun addAnnouncementReaction( + @Path("id") announcementId: String, + @Path("name") name: String + ): NetworkResult<Unit> + + @DELETE("api/v1/announcements/{id}/reactions/{name}") + suspend fun removeAnnouncementReaction( + @Path("id") announcementId: String, + @Path("name") name: String + ): NetworkResult<Unit> + + @FormUrlEncoded + @POST("api/v1/reports") + suspend fun report( + @Field("account_id") accountId: String, + @Field("status_ids[]") statusIds: List<String>, + @Field("comment") comment: String, + @Field("forward") isNotifyRemote: Boolean? + ): NetworkResult<Unit> + + @GET("api/v1/accounts/{id}/statuses") + suspend fun accountStatuses( + @Path("id") accountId: String, + @Query("max_id") maxId: String?, + @Query("since_id") sinceId: String?, + @Query("min_id") minId: String?, + @Query("limit") limit: Int?, + @Query("exclude_reblogs") excludeReblogs: Boolean? + ): NetworkResult<List<Status>> + + @GET("api/v2/search") + suspend fun search( + @Query("q") query: String?, + @Query("type") type: String? = null, + @Query("resolve") resolve: Boolean? = null, + @Query("limit") limit: Int? = null, + @Query("offset") offset: Int? = null, + @Query("following") following: Boolean? = null + ): NetworkResult<SearchResult> + + @FormUrlEncoded + @POST("api/v1/accounts/{id}/note") + suspend fun updateAccountNote( + @Path("id") accountId: String, + @Field("comment") note: String + ): NetworkResult<Relationship> + + @FormUrlEncoded + @POST("api/v1/push/subscription") + suspend fun subscribePushNotifications( + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @Field("subscription[endpoint]") endPoint: String, + @Field("subscription[keys][p256dh]") keysP256DH: String, + @Field("subscription[keys][auth]") keysAuth: String, + // The "data[alerts][]" fields to enable / disable notifications + // Should be generated dynamically from all the available notification + // types defined in [com.keylesspalace.tusky.entities.Notification.Types] + @FieldMap data: Map<String, Boolean> + ): NetworkResult<NotificationSubscribeResult> + + @FormUrlEncoded + @PUT("api/v1/push/subscription") + suspend fun updatePushNotificationSubscription( + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String, + @FieldMap data: Map<String, Boolean> + ): NetworkResult<NotificationSubscribeResult> + + @DELETE("api/v1/push/subscription") + suspend fun unsubscribePushNotifications( + @Header("Authorization") auth: String, + @Header(DOMAIN_HEADER) domain: String + ): NetworkResult<Unit> + + @GET("api/v1/tags/{name}") + suspend fun tag(@Path("name") name: String): NetworkResult<HashTag> + + @GET("api/v1/followed_tags") + suspend fun followedTags( + @Query("min_id") minId: String? = null, + @Query("since_id") sinceId: String? = null, + @Query("max_id") maxId: String? = null, + @Query("limit") limit: Int? = null + ): Response<List<HashTag>> + + @POST("api/v1/tags/{name}/follow") + suspend fun followTag(@Path("name") name: String): NetworkResult<HashTag> + + @POST("api/v1/tags/{name}/unfollow") + suspend fun unfollowTag(@Path("name") name: String): NetworkResult<HashTag> + + @GET("api/v1/trends/tags") + suspend fun trendingTags(): NetworkResult<List<TrendingTag>> + + @GET("api/v1/trends/statuses") + suspend fun trendingStatuses( + @Query("limit") limit: Int? = null, + @Query("offset") offset: String? = null + ): Response<List<Status>> + + @FormUrlEncoded + @POST("api/v1/statuses/{id}/translate") + suspend fun translate( + @Path("id") statusId: String, + @Field("lang") targetLanguage: String? + ): NetworkResult<Translation> +} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt b/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt new file mode 100644 index 0000000..0cfab9b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/network/MediaUploadApi.kt @@ -0,0 +1,21 @@ +package com.keylesspalace.tusky.network + +import com.keylesspalace.tusky.entity.MediaUploadResult +import okhttp3.MultipartBody +import retrofit2.Response +import retrofit2.http.Multipart +import retrofit2.http.POST +import retrofit2.http.Part + +/** endpoints defined in this interface will be called with a higher timeout than usual + * which is necessary for media uploads to succeed on some servers + */ +interface MediaUploadApi { + @Multipart + @POST("api/v2/media") + suspend fun uploadMedia( + @Part file: MultipartBody.Part, + @Part description: MultipartBody.Part? = null, + @Part focus: MultipartBody.Part? = null + ): Response<MediaUploadResult> +} diff --git a/app/src/main/java/com/keylesspalace/tusky/network/UriRequestBody.kt b/app/src/main/java/com/keylesspalace/tusky/network/UriRequestBody.kt new file mode 100644 index 0000000..bdd5a6e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/network/UriRequestBody.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2024 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ +package com.keylesspalace.tusky.network + +import android.content.ContentResolver +import android.net.Uri +import java.io.FileNotFoundException +import okhttp3.MediaType +import okhttp3.RequestBody +import okio.Buffer +import okio.BufferedSink +import okio.source + +// Align with Okio Segment size for better performance +private const val DEFAULT_CHUNK_SIZE = 8192L + +fun interface UploadCallback { + fun onProgressUpdate(percentage: Int) +} + +fun Uri.asRequestBody( + contentResolver: ContentResolver, + contentType: MediaType? = null, + contentLength: Long = -1L, + uploadListener: UploadCallback? = null +): RequestBody { + return object : RequestBody() { + override fun contentType(): MediaType? = contentType + + override fun contentLength(): Long = contentLength + + override fun writeTo(sink: BufferedSink) { + val buffer = Buffer() + var uploaded: Long = 0 + val inputStream = contentResolver.openInputStream(this@asRequestBody) + ?: throw FileNotFoundException("Unavailable ContentProvider") + + inputStream.source().use { source -> + while (true) { + val read = source.read(buffer, DEFAULT_CHUNK_SIZE) + if (read == -1L) { + break + } + sink.write(buffer, read) + uploaded += read + uploadListener?.let { if (contentLength > 0L) it.onProgressUpdate((100L * uploaded / contentLength).toInt()) } + } + uploadListener?.onProgressUpdate(100) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.kt new file mode 100644 index 0000000..80a30ad --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/pager/ImagePagerAdapter.kt @@ -0,0 +1,43 @@ +package com.keylesspalace.tusky.pager + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import com.keylesspalace.tusky.ViewMediaAdapter +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.fragment.ViewMediaFragment +import java.lang.ref.WeakReference + +class ImagePagerAdapter( + activity: FragmentActivity, + private val attachments: List<Attachment>, + private val initialPosition: Int +) : ViewMediaAdapter(activity) { + + private var didTransition = false + private val fragments = + MutableList<WeakReference<ViewMediaFragment>?>(attachments.size) { null } + + override fun getItemCount() = attachments.size + + override fun createFragment(position: Int): Fragment { + if (position >= 0 && position < attachments.size) { + // Fragment should not wait for or start transition if it already happened but we + // instantiate the same fragment again, e.g. open the first photo, scroll to the + // forth photo and then back to the first. The first fragment will try to start the + // transition and wait until it's over and it will never take place. + val fragment = ViewMediaFragment.newInstance( + attachment = attachments[position], + shouldStartPostponedTransition = !didTransition && position == initialPosition + ) + fragments[position] = WeakReference(fragment) + return fragment + } else { + throw IllegalStateException() + } + } + + override fun onTransitionEnd(position: Int) { + this.didTransition = true + fragments[position]?.get()?.onTransitionEnd() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt new file mode 100644 index 0000000..a8d9c70 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/pager/MainPagerAdapter.kt @@ -0,0 +1,33 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.pager + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import com.keylesspalace.tusky.TabData +import com.keylesspalace.tusky.util.CustomFragmentStateAdapter + +class MainPagerAdapter(var tabs: List<TabData>, activity: FragmentActivity) : CustomFragmentStateAdapter( + activity +) { + + override fun createFragment(position: Int): Fragment { + val tab = tabs[position] + return tab.fragment(tab.arguments) + } + + override fun getItemCount() = tabs.size +} diff --git a/app/src/main/java/com/keylesspalace/tusky/pager/SingleImagePagerAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/pager/SingleImagePagerAdapter.kt new file mode 100644 index 0000000..c1f5342 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/pager/SingleImagePagerAdapter.kt @@ -0,0 +1,25 @@ +package com.keylesspalace.tusky.pager + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import com.keylesspalace.tusky.ViewMediaAdapter +import com.keylesspalace.tusky.fragment.ViewMediaFragment + +class SingleImagePagerAdapter( + activity: FragmentActivity, + private val imageUrl: String +) : ViewMediaAdapter(activity) { + + override fun createFragment(position: Int): Fragment { + return if (position == 0) { + ViewMediaFragment.newSingleImageInstance(imageUrl) + } else { + throw IllegalStateException() + } + } + + override fun getItemCount() = 1 + + override fun onTransitionEnd(position: Int) { + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationBlockStateBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationBlockStateBroadcastReceiver.kt new file mode 100644 index 0000000..9df766b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/NotificationBlockStateBroadcastReceiver.kt @@ -0,0 +1,77 @@ +/* Copyright 2022 Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.receiver + +import android.app.NotificationManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import com.keylesspalace.tusky.components.systemnotifications.canEnablePushNotifications +import com.keylesspalace.tusky.components.systemnotifications.isUnifiedPushNotificationEnabledForAccount +import com.keylesspalace.tusky.components.systemnotifications.updateUnifiedPushSubscription +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.di.ApplicationScope +import com.keylesspalace.tusky.network.MastodonApi +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +@AndroidEntryPoint +class NotificationBlockStateBroadcastReceiver : BroadcastReceiver() { + @Inject + lateinit var mastodonApi: MastodonApi + + @Inject + lateinit var accountManager: AccountManager + + @Inject + @ApplicationScope + lateinit var externalScope: CoroutineScope + + override fun onReceive(context: Context, intent: Intent) { + if (Build.VERSION.SDK_INT < 28) return + if (!canEnablePushNotifications(context, accountManager)) return + + val nm = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + + val gid = when (intent.action) { + NotificationManager.ACTION_NOTIFICATION_CHANNEL_BLOCK_STATE_CHANGED -> { + val channelId = intent.getStringExtra(NotificationManager.EXTRA_NOTIFICATION_CHANNEL_ID) + nm.getNotificationChannel(channelId).group + } + NotificationManager.ACTION_NOTIFICATION_CHANNEL_GROUP_BLOCK_STATE_CHANGED -> { + intent.getStringExtra(NotificationManager.EXTRA_NOTIFICATION_CHANNEL_GROUP_ID) + } + else -> null + } ?: return + + accountManager.getAccountByIdentifier(gid)?.let { account -> + if (isUnifiedPushNotificationEnabledForAccount(account)) { + // Update UnifiedPush notification subscription + externalScope.launch { + updateUnifiedPushSubscription( + context, + mastodonApi, + accountManager, + account + ) + } + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt new file mode 100644 index 0000000..16de959 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/SendStatusBroadcastReceiver.kt @@ -0,0 +1,143 @@ +/* Copyright 2018 Jeremiasz Nelz <remi6397(a)gmail.com> + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.receiver + +import android.annotation.SuppressLint +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.core.app.RemoteInput +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.service.SendStatusService +import com.keylesspalace.tusky.service.StatusToSend +import com.keylesspalace.tusky.util.getSerializableExtraCompat +import com.keylesspalace.tusky.util.randomAlphanumericString +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +private const val TAG = "SendStatusBR" + +@AndroidEntryPoint +class SendStatusBroadcastReceiver : BroadcastReceiver() { + + @Inject + lateinit var accountManager: AccountManager + + @SuppressLint("MissingPermission") + override fun onReceive(context: Context, intent: Intent) { + if (intent.action == NotificationHelper.REPLY_ACTION) { + val serverNotificationId = intent.getStringExtra(NotificationHelper.KEY_SERVER_NOTIFICATION_ID) + val senderId = intent.getLongExtra(NotificationHelper.KEY_SENDER_ACCOUNT_ID, -1) + val senderIdentifier = intent.getStringExtra( + NotificationHelper.KEY_SENDER_ACCOUNT_IDENTIFIER + ) + val senderFullName = intent.getStringExtra( + NotificationHelper.KEY_SENDER_ACCOUNT_FULL_NAME + ) + val citedStatusId = intent.getStringExtra(NotificationHelper.KEY_CITED_STATUS_ID) + val visibility = + intent.getSerializableExtraCompat<Status.Visibility>(NotificationHelper.KEY_VISIBILITY)!! + val spoiler = intent.getStringExtra(NotificationHelper.KEY_SPOILER).orEmpty() + val mentions = intent.getStringArrayExtra(NotificationHelper.KEY_MENTIONS).orEmpty() + + val account = accountManager.getAccountById(senderId) + + val notificationManager = NotificationManagerCompat.from(context) + + val message = getReplyMessage(intent) + + if (account == null) { + Log.w(TAG, "Account \"$senderId\" not found in database. Aborting quick reply!") + + val notification = NotificationCompat.Builder( + context, + NotificationHelper.CHANNEL_MENTION + senderIdentifier + ) + .setSmallIcon(R.drawable.ic_notify) + .setColor(context.getColor(R.color.tusky_blue)) + .setGroup(senderFullName) + .setDefaults(0) // We don't want this to make any sound or vibration + .setOnlyAlertOnce(true) + .setContentTitle(context.getString(R.string.error_generic)) + .setContentText(context.getString(R.string.error_sender_account_gone)) + .setSubText(senderFullName) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setCategory(NotificationCompat.CATEGORY_SOCIAL) + .build() + + notificationManager.notify(serverNotificationId, senderId.toInt(), notification) + } else { + val text = mentions.joinToString(" ", postfix = " ") { "@$it" } + message.toString() + + val sendIntent = SendStatusService.sendStatusIntent( + context, + StatusToSend( + text = text, + warningText = spoiler, + visibility = visibility.serverString, + sensitive = false, + media = emptyList(), + scheduledAt = null, + inReplyToId = citedStatusId, + poll = null, + replyingStatusContent = null, + replyingStatusAuthorUsername = null, + accountId = account.id, + draftId = -1, + idempotencyKey = randomAlphanumericString(16), + retries = 0, + language = null, + statusId = null + ) + ) + + context.startService(sendIntent) + + // Notifications with remote input active can't be cancelled, so let's replace it with another one that will dismiss automatically + val notification = NotificationCompat.Builder( + context, + NotificationHelper.CHANNEL_MENTION + senderIdentifier + ) + .setSmallIcon(R.drawable.ic_notify) + .setColor(context.getColor(R.color.notification_color)) + .setGroup(senderFullName) + .setDefaults(0) // We don't want this to make any sound or vibration + .setOnlyAlertOnce(true) + .setContentTitle(context.getString(R.string.reply_sending)) + .setContentText(context.getString(R.string.reply_sending_long)) + .setSubText(senderFullName) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setCategory(NotificationCompat.CATEGORY_SOCIAL) + .setTimeoutAfter(5000) + .build() + + notificationManager.notify(serverNotificationId, senderId.toInt(), notification) + } + } + } + + private fun getReplyMessage(intent: Intent): CharSequence { + val remoteInput = RemoteInput.getResultsFromIntent(intent) + + return remoteInput?.getCharSequence(NotificationHelper.KEY_REPLY, "") ?: "" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt b/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt new file mode 100644 index 0000000..f8a7c8d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/receiver/UnifiedPushBroadcastReceiver.kt @@ -0,0 +1,75 @@ +/* Copyright 2022 Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.receiver + +import android.content.Context +import android.util.Log +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import com.keylesspalace.tusky.components.systemnotifications.registerUnifiedPushEndpoint +import com.keylesspalace.tusky.components.systemnotifications.unregisterUnifiedPushEndpoint +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.di.ApplicationScope +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.worker.NotificationWorker +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch +import org.unifiedpush.android.connector.MessagingReceiver + +@AndroidEntryPoint +class UnifiedPushBroadcastReceiver : MessagingReceiver() { + companion object { + const val TAG = "UnifiedPush" + } + + @Inject + lateinit var accountManager: AccountManager + + @Inject + lateinit var mastodonApi: MastodonApi + + @Inject + @ApplicationScope + lateinit var externalScope: CoroutineScope + + override fun onMessage(context: Context, message: ByteArray, instance: String) { + Log.d(TAG, "New message received for account $instance") + val workManager = WorkManager.getInstance(context) + val request = OneTimeWorkRequest.from(NotificationWorker::class.java) + workManager.enqueue(request) + } + + override fun onNewEndpoint(context: Context, endpoint: String, instance: String) { + Log.d(TAG, "Endpoint available for account $instance: $endpoint") + accountManager.getAccountById(instance.toLong())?.let { + externalScope.launch { + registerUnifiedPushEndpoint(context, mastodonApi, accountManager, it, endpoint) + } + } + } + + override fun onRegistrationFailed(context: Context, instance: String) = Unit + + override fun onUnregistered(context: Context, instance: String) { + Log.d(TAG, "Endpoint unregistered for account $instance") + accountManager.getAccountById(instance.toLong())?.let { + // It's fine if the account does not exist anymore -- that means it has been logged out + externalScope.launch { unregisterUnifiedPushEndpoint(mastodonApi, accountManager, it) } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt new file mode 100644 index 0000000..9d77acd --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/service/SendStatusService.kt @@ -0,0 +1,517 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.service + +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.ClipData +import android.content.ClipDescription +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import android.os.Parcelable +import android.util.Log +import androidx.annotation.StringRes +import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat +import at.connyduck.calladapter.networkresult.fold +import com.keylesspalace.tusky.MainActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.StatusChangedEvent +import com.keylesspalace.tusky.appstore.StatusComposedEvent +import com.keylesspalace.tusky.appstore.StatusScheduledEvent +import com.keylesspalace.tusky.components.compose.MediaUploader +import com.keylesspalace.tusky.components.compose.UploadEvent +import com.keylesspalace.tusky.components.drafts.DraftHelper +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.MediaAttribute +import com.keylesspalace.tusky.entity.NewPoll +import com.keylesspalace.tusky.entity.NewStatus +import com.keylesspalace.tusky.entity.ScheduledStatus +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.getParcelableExtraCompat +import com.keylesspalace.tusky.util.unsafeLazy +import dagger.hilt.android.AndroidEntryPoint +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.TimeUnit +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.parcelize.Parcelize +import retrofit2.HttpException + +@AndroidEntryPoint +class SendStatusService : Service() { + + @Inject + lateinit var mastodonApi: MastodonApi + + @Inject + lateinit var accountManager: AccountManager + + @Inject + lateinit var eventHub: EventHub + + @Inject + lateinit var draftHelper: DraftHelper + + @Inject + lateinit var mediaUploader: MediaUploader + + private val supervisorJob = SupervisorJob() + private val serviceScope = CoroutineScope(Dispatchers.Main + supervisorJob) + + private val statusesToSend = ConcurrentHashMap<Int, StatusToSend>() + private val sendJobs = ConcurrentHashMap<Int, Job>() + + private val notificationManager by unsafeLazy { + getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + } + + override fun onBind(intent: Intent): IBinder? = null + + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { + if (intent.hasExtra(KEY_STATUS)) { + val statusToSend: StatusToSend = intent.getParcelableExtraCompat(KEY_STATUS) + ?: throw IllegalStateException("SendStatusService started without $KEY_STATUS extra") + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = + NotificationChannel( + CHANNEL_ID, + getString(R.string.send_post_notification_channel_name), + NotificationManager.IMPORTANCE_LOW + ) + notificationManager.createNotificationChannel(channel) + } + + var notificationText = statusToSend.warningText + if (notificationText.isBlank()) { + notificationText = statusToSend.text + } + + val builder = NotificationCompat.Builder(this, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notify) + .setContentTitle(getString(R.string.send_post_notification_title)) + .setContentText(notificationText) + .setProgress(1, 0, true) + .setOngoing(true) + .setColor(getColor(R.color.notification_color)) + .addAction( + 0, + getString(android.R.string.cancel), + cancelSendingIntent(sendingNotificationId) + ) + + if (statusesToSend.size == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH) + startForeground(sendingNotificationId, builder.build()) + } else { + notificationManager.notify(sendingNotificationId, builder.build()) + } + + statusesToSend[sendingNotificationId] = statusToSend + sendStatus(sendingNotificationId--) + } else if (intent.hasExtra(KEY_CANCEL)) { + cancelSending(intent.getIntExtra(KEY_CANCEL, 0)) + } + + return START_NOT_STICKY + } + + override fun onTimeout(startId: Int) { + // https://developer.android.com/about/versions/14/changes/fgs-types-required#short-service + // max time for short service reached on Android 14+, stop sending + statusesToSend.forEach { (statusId, _) -> + serviceScope.launch { + failSending(statusId) + } + } + } + + private fun sendStatus(statusId: Int) { + // when statusToSend == null, sending has been canceled + val statusToSend = statusesToSend[statusId] ?: return + + // when account == null, user has logged out, cancel sending + val account = accountManager.getAccountById(statusToSend.accountId) + + if (account == null) { + statusesToSend.remove(statusId) + notificationManager.cancel(statusId) + stopSelfWhenDone() + return + } + + statusToSend.retries++ + + sendJobs[statusId] = serviceScope.launch { + // first, wait for media uploads to finish + val media = statusToSend.media.map { mediaItem -> + if (mediaItem.id == null) { + when (val uploadState = mediaUploader.getMediaUploadState(mediaItem.localId)) { + is UploadEvent.FinishedEvent -> mediaItem.copy(id = uploadState.mediaId, processed = uploadState.processed) + is UploadEvent.ErrorEvent -> { + Log.w(TAG, "failed uploading media", uploadState.error) + failSending(statusId) + stopSelfWhenDone() + return@launch + } + } + } else { + mediaItem + } + } + + // then wait until server finished processing the media + try { + var mediaCheckRetries = 0 + while (media.any { mediaItem -> !mediaItem.processed }) { + delay(1000L * mediaCheckRetries) + media.forEach { mediaItem -> + if (!mediaItem.processed) { + when (mastodonApi.getMedia(mediaItem.id!!).code()) { + 200 -> mediaItem.processed = true // success + 206 -> { } // media is still being processed, continue checking + else -> { // some kind of server error, retrying probably doesn't make sense + failSending(statusId) + stopSelfWhenDone() + return@launch + } + } + } + } + mediaCheckRetries++ + } + } catch (e: Exception) { + Log.w(TAG, "failed getting media status", e) + retrySending(statusId) + return@launch + } + + val isNew = statusToSend.statusId == null + + if (isNew) { + media.forEach { mediaItem -> + if (mediaItem.processed && (mediaItem.description != null || mediaItem.focus != null)) { + mastodonApi.updateMedia(mediaItem.id!!, mediaItem.description, mediaItem.focus?.toMastodonApiString()) + .fold({ + }, { throwable -> + Log.w(TAG, "failed to update media on status send", throwable) + failOrRetry(throwable, statusId) + + return@launch + }) + } + } + } + + // finally, send the new status + val newStatus = NewStatus( + status = statusToSend.text, + warningText = statusToSend.warningText, + inReplyToId = statusToSend.inReplyToId, + visibility = statusToSend.visibility, + sensitive = statusToSend.sensitive, + mediaIds = media.map { it.id!! }, + scheduledAt = statusToSend.scheduledAt, + poll = statusToSend.poll, + language = statusToSend.language, + mediaAttributes = media.map { mediaItem -> + MediaAttribute( + id = mediaItem.id!!, + description = mediaItem.description, + focus = mediaItem.focus?.toMastodonApiString(), + thumbnail = null + ) + } + ) + + val scheduled = !statusToSend.scheduledAt.isNullOrEmpty() + + val sendResult = if (isNew) { + if (!scheduled) { + mastodonApi.createStatus( + "Bearer " + account.accessToken, + account.domain, + statusToSend.idempotencyKey, + newStatus + ) + } else { + mastodonApi.createScheduledStatus( + "Bearer " + account.accessToken, + account.domain, + statusToSend.idempotencyKey, + newStatus + ) + } + } else { + mastodonApi.editStatus( + statusToSend.statusId!!, + "Bearer " + account.accessToken, + account.domain, + statusToSend.idempotencyKey, + newStatus + ) + } + + sendResult.fold({ sentStatus -> + statusesToSend.remove(statusId) + // If the status was loaded from a draft, delete the draft and associated media files. + if (statusToSend.draftId != 0) { + draftHelper.deleteDraftAndAttachments(statusToSend.draftId) + } + + mediaUploader.cancelUploadScope(*statusToSend.media.map { it.localId }.toIntArray()) + + if (scheduled) { + eventHub.dispatch(StatusScheduledEvent(sentStatus as ScheduledStatus)) + } else if (!isNew) { + eventHub.dispatch(StatusChangedEvent(sentStatus as Status)) + } else { + eventHub.dispatch(StatusComposedEvent(sentStatus as Status)) + } + + notificationManager.cancel(statusId) + }, { throwable -> + Log.w(TAG, "failed sending status", throwable) + failOrRetry(throwable, statusId) + }) + stopSelfWhenDone() + } + } + + private suspend fun failOrRetry(throwable: Throwable, statusId: Int) { + if (throwable is HttpException) { + // the server refused to accept, save status & show error message + failSending(statusId) + } else { + // a network problem occurred, let's retry sending the status + retrySending(statusId) + } + } + + private suspend fun retrySending(statusId: Int) { + // when statusToSend == null, sending has been canceled + val statusToSend = statusesToSend[statusId] ?: return + + val backoff = TimeUnit.SECONDS.toMillis( + statusToSend.retries.toLong() + ).coerceAtMost(MAX_RETRY_INTERVAL) + + delay(backoff) + sendStatus(statusId) + } + + private fun stopSelfWhenDone() { + if (statusesToSend.isEmpty()) { + ServiceCompat.stopForeground( + this@SendStatusService, + ServiceCompat.STOP_FOREGROUND_REMOVE + ) + stopSelf() + } + } + + private suspend fun failSending(statusId: Int) { + val failedStatus = statusesToSend.remove(statusId) + if (failedStatus != null) { + mediaUploader.cancelUploadScope(*failedStatus.media.map { it.localId }.toIntArray()) + + saveStatusToDrafts(failedStatus, failedToSendAlert = true) + + val notification = buildDraftNotification( + R.string.send_post_notification_error_title, + R.string.send_post_notification_saved_content, + failedStatus.accountId, + statusId + ) + + notificationManager.cancel(statusId) + notificationManager.notify(errorNotificationId++, notification) + } + + // NOTE only this removes the "Sending..." notification (added with startForeground() above) + stopSelfWhenDone() + } + + private fun cancelSending(statusId: Int) = serviceScope.launch { + val statusToCancel = statusesToSend.remove(statusId) + if (statusToCancel != null) { + mediaUploader.cancelUploadScope(*statusToCancel.media.map { it.localId }.toIntArray()) + + val sendJob = sendJobs.remove(statusId) + sendJob?.cancel() + + saveStatusToDrafts(statusToCancel, failedToSendAlert = false) + + val notification = buildDraftNotification( + R.string.send_post_notification_cancel_title, + R.string.send_post_notification_saved_content, + statusToCancel.accountId, + statusId + ) + + notificationManager.notify(statusId, notification) + + delay(5000) + + stopSelfWhenDone() + } + } + + private suspend fun saveStatusToDrafts(status: StatusToSend, failedToSendAlert: Boolean) { + draftHelper.saveDraft( + draftId = status.draftId, + accountId = status.accountId, + inReplyToId = status.inReplyToId, + content = status.text, + contentWarning = status.warningText, + sensitive = status.sensitive, + visibility = Status.Visibility.byString(status.visibility), + mediaUris = status.media.map { it.uri }, + mediaDescriptions = status.media.map { it.description }, + mediaFocus = status.media.map { it.focus }, + poll = status.poll, + failedToSend = true, + failedToSendAlert = failedToSendAlert, + scheduledAt = status.scheduledAt, + language = status.language, + statusId = status.statusId + ) + } + + private fun cancelSendingIntent(statusId: Int): PendingIntent { + val intent = Intent(this, SendStatusService::class.java) + intent.putExtra(KEY_CANCEL, statusId) + return PendingIntent.getService( + this, + statusId, + intent, + NotificationHelper.pendingIntentFlags(false) + ) + } + + private fun buildDraftNotification( + @StringRes title: Int, + @StringRes content: Int, + accountId: Long, + statusId: Int + ): Notification { + val intent = MainActivity.draftIntent(this, accountId) + + val pendingIntent = PendingIntent.getActivity( + this, + statusId, + intent, + NotificationHelper.pendingIntentFlags(false) + ) + + return NotificationCompat.Builder(this@SendStatusService, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notify) + .setContentTitle(getString(title)) + .setContentText(getString(content)) + .setColor(getColor(R.color.notification_color)) + .setAutoCancel(true) + .setOngoing(false) + .setContentIntent(pendingIntent) + .build() + } + + override fun onDestroy() { + super.onDestroy() + supervisorJob.cancel() + } + + companion object { + private const val TAG = "SendStatusService" + + private const val KEY_STATUS = "status" + private const val KEY_CANCEL = "cancel_id" + private const val CHANNEL_ID = "send_toots" + + private val MAX_RETRY_INTERVAL = TimeUnit.MINUTES.toMillis(1) + + private var sendingNotificationId = -1 // use negative ids to not clash with other notis + private var errorNotificationId = Int.MIN_VALUE // use even more negative ids to not clash with other notis + + fun sendStatusIntent(context: Context, statusToSend: StatusToSend): Intent { + val intent = Intent(context, SendStatusService::class.java) + intent.putExtra(KEY_STATUS, statusToSend) + + if (statusToSend.media.isNotEmpty()) { + // forward uri permissions + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + val uriClip = ClipData( + ClipDescription("Status Media", arrayOf("image/*", "video/*")), + ClipData.Item(statusToSend.media[0].uri) + ) + statusToSend.media + .drop(1) + .forEach { mediaItem -> + uriClip.addItem(ClipData.Item(mediaItem.uri)) + } + + intent.clipData = uriClip + } + + return intent + } + } +} + +@Parcelize +data class StatusToSend( + val text: String, + val warningText: String, + val visibility: String, + val sensitive: Boolean, + val media: List<MediaToSend>, + val scheduledAt: String?, + val inReplyToId: String?, + val poll: NewPoll?, + val replyingStatusContent: String?, + val replyingStatusAuthorUsername: String?, + val accountId: Long, + val draftId: Int, + val idempotencyKey: String, + var retries: Int, + val language: String?, + val statusId: String? +) : Parcelable + +@Parcelize +data class MediaToSend( + val localId: Int, + // null if media is not yet completely uploaded + val id: String?, + val uri: String, + val description: String?, + val focus: Attachment.Focus?, + var processed: Boolean +) : Parcelable diff --git a/app/src/main/java/com/keylesspalace/tusky/service/ServiceClient.kt b/app/src/main/java/com/keylesspalace/tusky/service/ServiceClient.kt new file mode 100644 index 0000000..0269b66 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/service/ServiceClient.kt @@ -0,0 +1,28 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.service + +import android.content.Context +import androidx.core.content.ContextCompat +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class ServiceClient @Inject constructor(@ApplicationContext private val context: Context) { + fun sendToot(tootToSend: StatusToSend) { + val intent = SendStatusService.sendStatusIntent(context, tootToSend) + ContextCompat.startForegroundService(context, intent) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/service/TuskyTileService.kt b/app/src/main/java/com/keylesspalace/tusky/service/TuskyTileService.kt new file mode 100644 index 0000000..86d6407 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/service/TuskyTileService.kt @@ -0,0 +1,45 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.service + +import android.annotation.SuppressLint +import android.app.PendingIntent +import android.content.Intent +import android.os.Build +import android.service.quicksettings.TileService +import com.keylesspalace.tusky.MainActivity +import com.keylesspalace.tusky.components.compose.ComposeActivity + +/** + * Small Addition that adds in a QuickSettings tile + * opens the Compose activity or shows an account selector when multiple accounts are present + */ +class TuskyTileService : TileService() { + + @SuppressLint("StartActivityAndCollapseDeprecated") + @Suppress("DEPRECATION") + override fun onClick() { + val intent = MainActivity.composeIntent(this, ComposeActivity.ComposeOptions()) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + val pendingIntent = PendingIntent.getActivity(this, 1, intent, PendingIntent.FLAG_IMMUTABLE) + startActivityAndCollapse(pendingIntent) + } else { + startActivityAndCollapse(intent) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/AccountPreferenceDataStore.kt b/app/src/main/java/com/keylesspalace/tusky/settings/AccountPreferenceDataStore.kt new file mode 100644 index 0000000..d62731f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/settings/AccountPreferenceDataStore.kt @@ -0,0 +1,48 @@ +package com.keylesspalace.tusky.settings + +import androidx.preference.PreferenceDataStore +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.PreferenceChangedEvent +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.di.ApplicationScope +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class AccountPreferenceDataStore @Inject constructor( + private val accountManager: AccountManager, + private val eventHub: EventHub, + @ApplicationScope private val externalScope: CoroutineScope +) : PreferenceDataStore() { + private val account: AccountEntity = accountManager.activeAccount!! + + override fun getBoolean(key: String, defValue: Boolean): Boolean { + return when (key) { + PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA -> account.alwaysShowSensitiveMedia + PrefKeys.ALWAYS_OPEN_SPOILER -> account.alwaysOpenSpoiler + PrefKeys.MEDIA_PREVIEW_ENABLED -> account.mediaPreviewEnabled + PrefKeys.TAB_FILTER_HOME_BOOSTS -> account.isShowHomeBoosts + PrefKeys.TAB_FILTER_HOME_REPLIES -> account.isShowHomeReplies + PrefKeys.TAB_SHOW_HOME_SELF_BOOSTS -> account.isShowHomeSelfBoosts + else -> defValue + } + } + + override fun putBoolean(key: String, value: Boolean) { + when (key) { + PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA -> account.alwaysShowSensitiveMedia = value + PrefKeys.ALWAYS_OPEN_SPOILER -> account.alwaysOpenSpoiler = value + PrefKeys.MEDIA_PREVIEW_ENABLED -> account.mediaPreviewEnabled = value + PrefKeys.TAB_FILTER_HOME_BOOSTS -> account.isShowHomeBoosts = value + PrefKeys.TAB_FILTER_HOME_REPLIES -> account.isShowHomeReplies = value + PrefKeys.TAB_SHOW_HOME_SELF_BOOSTS -> account.isShowHomeSelfBoosts = value + } + + accountManager.saveAccount(account) + + externalScope.launch { + eventHub.dispatch(PreferenceChangedEvent(key)) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/ProxyConfiguration.kt b/app/src/main/java/com/keylesspalace/tusky/settings/ProxyConfiguration.kt new file mode 100644 index 0000000..db8046c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/settings/ProxyConfiguration.kt @@ -0,0 +1,43 @@ +package com.keylesspalace.tusky.settings + +import java.net.IDN + +class ProxyConfiguration private constructor( + val hostname: String, + val port: Int +) { + companion object { + fun create(hostname: String, port: Int): ProxyConfiguration? { + if (isValidHostname(IDN.toASCII(hostname)) && isValidProxyPort(port)) { + return ProxyConfiguration(hostname, port) + } + return null + } + fun isValidProxyPort(value: Any): Boolean = when (value) { + is String -> if (value == "") { + true + } else { + value.runCatching(String::toInt).map( + PROXY_RANGE::contains + ).getOrDefault(false) + } + is Int -> PROXY_RANGE.contains(value) + else -> false + } + fun isValidHostname(hostname: String): Boolean = + IP_ADDRESS_REGEX.matches(hostname) || HOSTNAME_REGEX.matches(hostname) + const val MIN_PROXY_PORT = 1 + const val MAX_PROXY_PORT = 65535 + } +} + +private val PROXY_RANGE = + IntRange(ProxyConfiguration.MIN_PROXY_PORT, ProxyConfiguration.MAX_PROXY_PORT) +private val IP_ADDRESS_REGEX = + Regex( + "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$" + ) +private val HOSTNAME_REGEX = + Regex( + "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\-]*[a-zA-Z0-9])\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\-]*[A-Za-z0-9])$" + ) diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt new file mode 100644 index 0000000..4861f59 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsConstants.kt @@ -0,0 +1,119 @@ +package com.keylesspalace.tusky.settings + +enum class AppTheme(val value: String) { + NIGHT("night"), + DAY("day"), + BLACK("black"), + AUTO("auto"), + AUTO_SYSTEM("auto_system"), + AUTO_SYSTEM_BLACK("auto_system_black"); + + companion object { + fun stringValues() = entries.map { it.value }.toTypedArray() + + @JvmField + val DEFAULT = AUTO_SYSTEM + } +} + +/** + * Current preferences schema version. Format is 4-digit year + 2 digit month (zero padded) + 2 + * digit day (zero padded) + 2 digit counter (zero padded). + * + * If you make an incompatible change to the preferences schema you must: + * + * - Update this value + * - Update the code in + * [TuskyApplication.upgradeSharedPreferences][com.keylesspalace.tusky.TuskyApplication.upgradeSharedPreferences] + * to migrate from the old schema version to the new schema version. + * + * An incompatible change is: + * + * - Deleting a preference. The migration should delete the old preference. + * - Changing a preference's default value (e.g., from true to false, or from one enum value to + * another). The migration should check to see if the user had set an explicit value for + * that preference ([SharedPreferences.contains][android.content.SharedPreferences.contains]); + * if they hadn't then the migration should set the *old* default value as the preference's + * value, so the app behaviour does not unexpectedly change. + * - Changing a preference's type (e.g,. from a boolean to an enum). If you do this you may want + * to give the preference a different name, but you still need to migrate the user's previous + * preference value to the new preference. + * - Renaming a preference key. The migration should copy the user's previous value for the + * preference under the old key to the value for the new, and delete the old preference. + * + * A compatible change is: + * + * - Adding a new preference that does not change the interpretation of an existing preference + */ +const val SCHEMA_VERSION = 2023112001 + +/** The schema version for fresh installs */ +const val NEW_INSTALL_SCHEMA_VERSION = 0 + +object PrefKeys { + // Note: not all of these keys are actually used as SharedPreferences keys but we must give + // each preference a key for it to work. + + const val SCHEMA_VERSION: String = "schema_version" + const val APP_THEME = "appTheme" + const val LANGUAGE = "language" + const val STATUS_TEXT_SIZE = "statusTextSize" + const val READING_ORDER = "readingOrder" + const val MAIN_NAV_POSITION = "mainNavPosition" + const val HIDE_TOP_TOOLBAR = "hideTopToolbar" + const val SHOW_NOTIFICATIONS_FILTER = "showNotificationsFilter" + const val ABSOLUTE_TIME_VIEW = "absoluteTimeView" + const val SHOW_BOT_OVERLAY = "showBotOverlay" + const val ANIMATE_GIF_AVATARS = "animateGifAvatars" + const val USE_BLURHASH = "useBlurhash" + const val SHOW_SELF_USERNAME = "showSelfUsername" + const val SHOW_CARDS_IN_TIMELINES = "showCardsInTimelines" + const val CONFIRM_REBLOGS = "confirmReblogs" + const val CONFIRM_FAVOURITES = "confirmFavourites" + const val CONFIRM_FOLLOWS = "confirmFollows" + const val ENABLE_SWIPE_FOR_TABS = "enableSwipeForTabs" + const val ANIMATE_CUSTOM_EMOJIS = "animateCustomEmojis" + const val SHOW_STATS_INLINE = "showStatsInline" + + const val CUSTOM_TABS = "customTabs" + const val WELLBEING_LIMITED_NOTIFICATIONS = "wellbeingModeLimitedNotifications" + const val WELLBEING_HIDE_STATS_POSTS = "wellbeingHideStatsPosts" + const val WELLBEING_HIDE_STATS_PROFILE = "wellbeingHideStatsProfile" + + const val HTTP_PROXY_ENABLED = "httpProxyEnabled" + const val HTTP_PROXY_SERVER = "httpProxyServer" + const val HTTP_PROXY_PORT = "httpProxyPort" + + const val DEFAULT_POST_PRIVACY = "defaultPostPrivacy" + const val DEFAULT_POST_LANGUAGE = "defaultPostLanguage" + const val DEFAULT_REPLY_PRIVACY = "defaultReplyPrivacy" + const val DEFAULT_MEDIA_SENSITIVITY = "defaultMediaSensitivity" + const val MEDIA_PREVIEW_ENABLED = "mediaPreviewEnabled" + const val ALWAYS_SHOW_SENSITIVE_MEDIA = "alwaysShowSensitiveMedia" + const val ALWAYS_OPEN_SPOILER = "alwaysOpenSpoiler" + + const val NOTIFICATIONS_ENABLED = "notificationsEnabled" + const val NOTIFICATION_ALERT_LIGHT = "notificationAlertLight" + const val NOTIFICATION_ALERT_VIBRATE = "notificationAlertVibrate" + const val NOTIFICATION_ALERT_SOUND = "notificationAlertSound" + const val NOTIFICATION_FILTER_POLLS = "notificationFilterPolls" + const val NOTIFICATION_FILTER_FAVS = "notificationFilterFavourites" + const val NOTIFICATION_FILTER_REBLOGS = "notificationFilterReblogs" + const val NOTIFICATION_FILTER_FOLLOW_REQUESTS = "notificationFilterFollowRequests" + const val NOTIFICATIONS_FILTER_FOLLOWS = "notificationFilterFollows" + const val NOTIFICATION_FILTER_SUBSCRIPTIONS = "notificationFilterSubscriptions" + const val NOTIFICATION_FILTER_SIGN_UPS = "notificationFilterSignUps" + const val NOTIFICATION_FILTER_UPDATES = "notificationFilterUpdates" + const val NOTIFICATION_FILTER_REPORTS = "notificationFilterReports" + + const val TAB_FILTER_HOME_REPLIES = "tabFilterHomeReplies_v2" // This was changed once to reset an unintentionally set default. + const val TAB_FILTER_HOME_BOOSTS = "tabFilterHomeBoosts" + const val TAB_SHOW_HOME_SELF_BOOSTS = "tabShowHomeSelfBoosts" + + /** UI text scaling factor, stored as float, 100 = 100% = no scaling */ + const val UI_TEXT_SCALE_RATIO = "uiTextScaleRatio" + + object Deprecated { + const val FAB_HIDE = "fabHide" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt new file mode 100644 index 0000000..c1b5742 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/settings/SettingsDSL.kt @@ -0,0 +1,113 @@ +package com.keylesspalace.tusky.settings + +import android.content.Context +import android.widget.Button +import androidx.activity.result.ActivityResultRegistryOwner +import androidx.annotation.StringRes +import androidx.core.widget.doAfterTextChanged +import androidx.lifecycle.LifecycleOwner +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceCategory +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceScreen +import androidx.preference.SwitchPreference +import com.keylesspalace.tusky.view.SliderPreference +import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference + +class PreferenceParent( + val context: Context, + val addPref: (pref: Preference) -> Unit +) + +inline fun PreferenceParent.preference(builder: Preference.() -> Unit): Preference { + val pref = Preference(context) + builder(pref) + addPref(pref) + return pref +} + +inline fun PreferenceParent.listPreference(builder: ListPreference.() -> Unit): ListPreference { + val pref = ListPreference(context) + builder(pref) + addPref(pref) + return pref +} + +inline fun <A> PreferenceParent.emojiPreference( + activity: A, + builder: EmojiPickerPreference.() -> Unit +): EmojiPickerPreference + where A : Context, A : ActivityResultRegistryOwner, A : LifecycleOwner { + val pref = EmojiPickerPreference.get(activity) + builder(pref) + addPref(pref) + return pref +} + +inline fun PreferenceParent.sliderPreference( + builder: SliderPreference.() -> Unit +): SliderPreference { + val pref = SliderPreference(context) + builder(pref) + addPref(pref) + return pref +} + +inline fun PreferenceParent.switchPreference( + builder: SwitchPreference.() -> Unit +): SwitchPreference { + val pref = SwitchPreference(context) + builder(pref) + addPref(pref) + return pref +} + +inline fun PreferenceParent.validatedEditTextPreference( + errorMessage: String?, + crossinline isValid: (a: String) -> Boolean, + builder: EditTextPreference.() -> Unit +): EditTextPreference { + val pref = EditTextPreference(context) + pref.setOnBindEditTextListener { editText -> + editText.doAfterTextChanged { editable -> + requireNotNull(editable) + val btn = editText.rootView.findViewById<Button>(android.R.id.button1) + if (isValid(editable.toString())) { + editText.error = null + btn.isEnabled = true + } else { + editText.error = errorMessage + btn.isEnabled = false + } + } + } + builder(pref) + addPref(pref) + return pref +} + +inline fun PreferenceParent.preferenceCategory( + @StringRes title: Int? = null, + builder: PreferenceParent.(PreferenceCategory) -> Unit +) { + val category = PreferenceCategory(context) + addPref(category) + title?.run(category::setTitle) + val newParent = PreferenceParent(context) { category.addPreference(it) } + builder(newParent, category) +} + +inline fun PreferenceFragmentCompat.makePreferenceScreen( + builder: PreferenceParent.() -> Unit +): PreferenceScreen { + val context = requireContext() + val screen = preferenceManager.createPreferenceScreen(context) + val parent = PreferenceParent(context) { screen.addPreference(it) } + // For some functions (like dependencies) it's much easier for us if we attach screen first + // and change it later + preferenceScreen = screen + builder(parent) + return screen +} diff --git a/app/src/main/java/com/keylesspalace/tusky/usecase/DeveloperToolsUseCase.kt b/app/src/main/java/com/keylesspalace/tusky/usecase/DeveloperToolsUseCase.kt new file mode 100644 index 0000000..f2d0d36 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/usecase/DeveloperToolsUseCase.kt @@ -0,0 +1,46 @@ +package com.keylesspalace.tusky.usecase + +import android.util.Log +import androidx.room.withTransaction +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.dao.TimelineDao +import javax.inject.Inject + +/** + * Functionality that is only intended to be used by the "Developer Tools" menu when built + * in debug mode. + */ +class DeveloperToolsUseCase @Inject constructor( + private val db: AppDatabase +) { + + private var timelineDao: TimelineDao = db.timelineDao() + + /** + * Create a gap in the home timeline to make it easier to interactively experiment with + * different "Load more" behaviours. + * + * Do this by taking the 10 most recent statuses, keeping the first 2, deleting the next 7, + * and replacing the last one with a placeholder. + */ + suspend fun createLoadMoreGap(accountId: Long) { + db.withTransaction { + val ids = timelineDao.getMostRecentNHomeTimelineIds(accountId, 10) + val maxId = ids[2] + val minId = ids[8] + val placeHolderId = ids[9] + + Log.d( + TAG, + "createLoadMoreGap: creating gap between $minId .. $maxId (new placeholder: $placeHolderId" + ) + + timelineDao.deleteRange(accountId, minId, maxId) + timelineDao.convertHomeTimelineItemToPlaceholder(placeHolderId) + } + } + + companion object { + const val TAG = "DeveloperToolsUseCase" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt b/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt new file mode 100644 index 0000000..e1556b0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/usecase/LogoutUsecase.kt @@ -0,0 +1,67 @@ +package com.keylesspalace.tusky.usecase + +import android.content.Context +import com.keylesspalace.tusky.components.drafts.DraftHelper +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper +import com.keylesspalace.tusky.components.systemnotifications.disableUnifiedPushNotificationsForAccount +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.DatabaseCleaner +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.ShareShortcutHelper +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject + +class LogoutUsecase @Inject constructor( + @ApplicationContext private val context: Context, + private val api: MastodonApi, + private val databaseCleaner: DatabaseCleaner, + private val accountManager: AccountManager, + private val draftHelper: DraftHelper, + private val shareShortcutHelper: ShareShortcutHelper +) { + + /** + * Logs the current account out and clears all caches associated with it + * @return true if the user is logged in with other accounts, false if it was the only one + */ + suspend fun logout(): Boolean { + accountManager.activeAccount?.let { activeAccount -> + + // invalidate the oauth token, if we have the client id & secret + // (could be missing if user logged in with a previous version of Tusky) + val clientId = activeAccount.clientId + val clientSecret = activeAccount.clientSecret + if (clientId != null && clientSecret != null) { + api.revokeOAuthToken( + clientId = clientId, + clientSecret = clientSecret, + token = activeAccount.accessToken + ) + } + + // disable push notifications + disableUnifiedPushNotificationsForAccount(context, activeAccount) + + // disable pull notifications + if (!NotificationHelper.areNotificationsEnabled(context, accountManager)) { + NotificationHelper.disablePullNotifications(context) + } + + // clear notification channels + NotificationHelper.deleteNotificationChannelsForAccount(activeAccount, context) + + // remove account from local AccountManager + val otherAccountAvailable = accountManager.logActiveAccountOut() != null + + // clear the database - this could trigger network calls so do it last when all tokens are gone + databaseCleaner.cleanupEverything(activeAccount.id) + draftHelper.deleteAllDraftsAndAttachmentsForAccount(activeAccount.id) + + // remove shortcut associated with the account + shareShortcutHelper.removeShortcut(activeAccount) + + return otherAccountAvailable + } + return false + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt b/app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt new file mode 100644 index 0000000..805b736 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/usecase/TimelineCases.kt @@ -0,0 +1,156 @@ +/* Copyright 2018 charlag + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.usecase + +import android.util.Log +import at.connyduck.calladapter.networkresult.NetworkResult +import at.connyduck.calladapter.networkresult.fold +import at.connyduck.calladapter.networkresult.onFailure +import at.connyduck.calladapter.networkresult.onSuccess +import com.keylesspalace.tusky.appstore.BlockEvent +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.MuteEvent +import com.keylesspalace.tusky.appstore.PollVoteEvent +import com.keylesspalace.tusky.appstore.StatusChangedEvent +import com.keylesspalace.tusky.appstore.StatusDeletedEvent +import com.keylesspalace.tusky.entity.DeletedStatus +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.Translation +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.getServerErrorMessage +import java.util.Locale +import javax.inject.Inject + +/** + * Created by charlag on 3/24/18. + */ + +class TimelineCases @Inject constructor( + private val mastodonApi: MastodonApi, + private val eventHub: EventHub +) { + + suspend fun reblog(statusId: String, reblog: Boolean): NetworkResult<Status> { + return if (reblog) { + mastodonApi.reblogStatus(statusId) + } else { + mastodonApi.unreblogStatus(statusId) + }.onSuccess { status -> + if (status.reblog != null) { + // when reblogging, the Mastodon Api does not return the reblogged status directly + // but the newly created status with reblog set to the reblogged status + eventHub.dispatch(StatusChangedEvent(status.reblog)) + } else { + eventHub.dispatch(StatusChangedEvent(status)) + } + } + } + + suspend fun favourite(statusId: String, favourite: Boolean): NetworkResult<Status> { + return if (favourite) { + mastodonApi.favouriteStatus(statusId) + } else { + mastodonApi.unfavouriteStatus(statusId) + }.onSuccess { status -> + eventHub.dispatch(StatusChangedEvent(status)) + } + } + + suspend fun bookmark(statusId: String, bookmark: Boolean): NetworkResult<Status> { + return if (bookmark) { + mastodonApi.bookmarkStatus(statusId) + } else { + mastodonApi.unbookmarkStatus(statusId) + }.onSuccess { status -> + eventHub.dispatch(StatusChangedEvent(status)) + } + } + + suspend fun muteConversation(statusId: String, mute: Boolean): NetworkResult<Status> { + return if (mute) { + mastodonApi.muteConversation(statusId) + } else { + mastodonApi.unmuteConversation(statusId) + }.onSuccess { status -> + eventHub.dispatch(StatusChangedEvent(status)) + } + } + + suspend fun mute(statusId: String, notifications: Boolean, duration: Int?) { + try { + mastodonApi.muteAccount(statusId, notifications, duration) + eventHub.dispatch(MuteEvent(statusId)) + } catch (t: Throwable) { + Log.w(TAG, "Failed to mute account", t) + } + } + + suspend fun block(statusId: String) { + try { + mastodonApi.blockAccount(statusId) + eventHub.dispatch(BlockEvent(statusId)) + } catch (t: Throwable) { + Log.w(TAG, "Failed to block account", t) + } + } + + suspend fun delete(statusId: String): NetworkResult<DeletedStatus> { + return mastodonApi.deleteStatus(statusId) + .onSuccess { eventHub.dispatch(StatusDeletedEvent(statusId)) } + .onFailure { Log.w(TAG, "Failed to delete status", it) } + } + + suspend fun pin(statusId: String, pin: Boolean): NetworkResult<Status> { + return if (pin) { + mastodonApi.pinStatus(statusId) + } else { + mastodonApi.unpinStatus(statusId) + }.fold({ status -> + eventHub.dispatch(StatusChangedEvent(status)) + NetworkResult.success(status) + }, { e -> + Log.w(TAG, "Failed to change pin state", e) + NetworkResult.failure(TimelineError(e.getServerErrorMessage())) + }) + } + + suspend fun voteInPoll( + statusId: String, + pollId: String, + choices: List<Int> + ): NetworkResult<Poll> { + if (choices.isEmpty()) { + return NetworkResult.failure(IllegalStateException()) + } + + return mastodonApi.voteInPoll(pollId, choices).onSuccess { poll -> + eventHub.dispatch(PollVoteEvent(statusId, poll)) + } + } + + suspend fun translate( + statusId: String + ): NetworkResult<Translation> { + return mastodonApi.translate(statusId, Locale.getDefault().language) + } + + companion object { + private const val TAG = "TimelineCases" + } +} + +class TimelineError(message: String?) : RuntimeException(message) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatter.kt b/app/src/main/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatter.kt new file mode 100644 index 0000000..dd40b84 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatter.kt @@ -0,0 +1,71 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +class AbsoluteTimeFormatter @JvmOverloads constructor(private val tz: TimeZone = TimeZone.getDefault()) { + private val sameDaySdf = SimpleDateFormat( + "HH:mm", + Locale.getDefault() + ).apply { this.timeZone = tz } + private val sameYearSdf = SimpleDateFormat("dd MMM, HH:mm", Locale.getDefault()).apply { + this.timeZone = tz + } + private val otherYearSdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).apply { + this.timeZone = tz + } + private val otherYearCompleteSdf = SimpleDateFormat( + "yyyy-MM-dd HH:mm", + Locale.getDefault() + ).apply { + this.timeZone = tz + } + + @JvmOverloads + fun format(time: Date?, shortFormat: Boolean = true, now: Date = Date()): String { + return when { + time == null -> "??" + isSameDate(time, now, tz) -> sameDaySdf.format(time) + isSameYear(time, now, tz) -> sameYearSdf.format(time) + shortFormat -> otherYearSdf.format(time) + else -> otherYearCompleteSdf.format(time) + } + } + + companion object { + + private fun isSameDate(dateOne: Date, dateTwo: Date, tz: TimeZone): Boolean { + val calendarOne = Calendar.getInstance(tz).apply { time = dateOne } + val calendarTwo = Calendar.getInstance(tz).apply { time = dateTwo } + + return calendarOne.get(Calendar.YEAR) == calendarTwo.get(Calendar.YEAR) && + calendarOne.get(Calendar.MONTH) == calendarTwo.get(Calendar.MONTH) && + calendarOne.get(Calendar.DAY_OF_MONTH) == calendarTwo.get(Calendar.DAY_OF_MONTH) + } + + private fun isSameYear(dateOne: Date, dateTwo: Date, timeZone1: TimeZone): Boolean { + val calendarOne = Calendar.getInstance(timeZone1).apply { time = dateOne } + val calendarTwo = Calendar.getInstance(timeZone1).apply { time = dateTwo } + + return calendarOne.get(Calendar.YEAR) == calendarTwo.get(Calendar.YEAR) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ActivityExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/ActivityExtensions.kt new file mode 100644 index 0000000..a58f4f4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ActivityExtensions.kt @@ -0,0 +1,65 @@ +@file:JvmName("ActivityExtensions") + +package com.keylesspalace.tusky.util + +import android.app.Activity +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.content.Intent +import android.os.Build +import android.widget.Toast +import androidx.activity.ComponentActivity +import androidx.annotation.AnimRes +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import com.keylesspalace.tusky.BaseActivity + +fun Activity.startActivityWithSlideInAnimation(intent: Intent) { + startActivity(intent.withSlideInAnimation()) +} + +fun Intent.withSlideInAnimation(): Intent { + // the new transition api needs to be called by the activity that is the result of the transition, + // so we pass a flag that BaseActivity will respect. + return putExtra(BaseActivity.OPEN_WITH_SLIDE_IN, true) +} + +/** + * Call this method in Activity.onCreate() to configure the open or close transitions. + */ +@Suppress("DEPRECATION") +fun ComponentActivity.overrideActivityTransitionCompat( + overrideType: Int, + @AnimRes enterAnim: Int, + @AnimRes exitAnim: Int +) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + overrideActivityTransition(overrideType, enterAnim, exitAnim) + } else { + if (overrideType == ActivityConstants.OVERRIDE_TRANSITION_OPEN) { + overridePendingTransition(enterAnim, exitAnim) + } else { + lifecycle.addObserver( + LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_PAUSE && isFinishing) { + overridePendingTransition(enterAnim, exitAnim) + } + } + ) + } + } +} + +fun Activity.copyToClipboard(text: CharSequence, popupText: CharSequence, clipboardLabel: CharSequence = "") { + val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard.setPrimaryClip(ClipData.newPlainText(clipboardLabel, text)) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + Toast.makeText(this, popupText, Toast.LENGTH_SHORT).show() + } +} + +object ActivityConstants { + const val OVERRIDE_TRANSITION_OPEN = 0 + const val OVERRIDE_TRANSITION_CLOSE = 1 +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/AlertDialogExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/AlertDialogExtensions.kt new file mode 100644 index 0000000..70362db --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/AlertDialogExtensions.kt @@ -0,0 +1,63 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.util + +import android.content.DialogInterface +import androidx.annotation.StringRes +import androidx.appcompat.app.AlertDialog +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * Wait for the alert dialog buttons to be clicked, return the ID of the clicked button + * + * @param positiveText Text to show on the positive button + * @param negativeText Optional text to show on the negative button + * @param neutralText Optional text to show on the neutral button + */ +@OptIn(ExperimentalCoroutinesApi::class) +suspend fun AlertDialog.await( + positiveText: String, + negativeText: String? = null, + neutralText: String? = null +) = suspendCancellableCoroutine { cont -> + val listener = DialogInterface.OnClickListener { _, which -> + cont.resume(which) { dismiss() } + } + + setButton(AlertDialog.BUTTON_POSITIVE, positiveText, listener) + negativeText?.let { setButton(AlertDialog.BUTTON_NEGATIVE, it, listener) } + neutralText?.let { setButton(AlertDialog.BUTTON_NEUTRAL, it, listener) } + + setOnCancelListener { cont.cancel() } + cont.invokeOnCancellation { dismiss() } + show() +} + +/** + * @see [AlertDialog.await] + */ +suspend fun AlertDialog.await( + @StringRes positiveTextResource: Int, + @StringRes negativeTextResource: Int? = null, + @StringRes neutralTextResource: Int? = null +) = await( + context.getString(positiveTextResource), + negativeTextResource?.let { context.getString(it) }, + neutralTextResource?.let { context.getString(it) } +) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/AsciiFolding.kt b/app/src/main/java/com/keylesspalace/tusky/util/AsciiFolding.kt new file mode 100644 index 0000000..a0f0735 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/AsciiFolding.kt @@ -0,0 +1,26 @@ +/* Copyright 2022 Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +// Inspired by https://github.com/mastodon/mastodon/blob/main/app/lib/ascii_folding.rb + +val unicodeToASCIIMap = "ÀÁÂÃÄÅàáâãäåĀāĂ㥹ÇçĆćĈĉĊċČčÐðĎďĐđÈÉÊËèéêëĒēĔĕĖėĘęĚěĜĝĞğĠġĢģĤĥĦħÌÍÎÏìíîïĨĩĪīĬĭĮįİıĴĵĶķĸĹĺĻļĽľĿŀŁłÑñŃńŅņŇňʼnŊŋÒÓÔÕÖØòóôõöøŌōŎŏŐőŔŕŖŗŘřŚśŜŝŞşŠšſŢţŤťŦŧÙÚÛÜùúûüŨũŪūŬŭŮůŰűŲųŴŵÝýÿŶŷŸŹźŻżŽž".toList().zip( + "AAAAAAaaaaaaAaAaAaCcCcCcCcCcDdDdDdEEEEeeeeEeEeEeEeEeGgGgGgGgHhHhIIIIiiiiIiIiIiIiIiJjKkkLlLlLlLlLlNnNnNnNnnNnOOOOOOooooooOoOoOoRrRrRrSsSsSsSssTtTtTtUUUUuuuuUuUuUuUuUuUuWwYyyYyYZzZzZz".toList() +).toMap() + +fun normalizeToASCII(text: CharSequence): String { + return String(text.map { unicodeToASCIIMap[it] ?: it }.toCharArray()) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/AttachmentHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/AttachmentHelper.kt new file mode 100644 index 0000000..287e3e7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/AttachmentHelper.kt @@ -0,0 +1,34 @@ +@file:JvmName("AttachmentHelper") + +package com.keylesspalace.tusky.util + +import android.content.Context +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Attachment +import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.seconds + +fun Attachment.getFormattedDescription(context: Context): CharSequence { + val durationInSeconds = meta?.duration ?: 0f + val duration = if (durationInSeconds > 0f) { + durationInSeconds.roundToInt().seconds.toComponents { hours, minutes, seconds, _ -> + "%d:%02d:%02d ".format(hours, minutes, seconds) + } + } else { + "" + } + return duration + if (description.isNullOrEmpty()) { + context.getString(R.string.description_post_media_no_description_placeholder) + } else { + description + } +} + +fun List<Attachment>.aspectRatios(): List<Double> { + return map { attachment -> + // clamp ratio between 2:1 & 1:2, defaulting to 16:9 + val size = (attachment.meta?.small ?: attachment.meta?.original) ?: return@map 1.7778 + val aspect = if (size.aspect > 0) size.aspect else size.width.toDouble() / size.height + aspect.coerceIn(0.5, 2.0) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BindingHolder.kt b/app/src/main/java/com/keylesspalace/tusky/util/BindingHolder.kt new file mode 100644 index 0000000..62167ee --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/BindingHolder.kt @@ -0,0 +1,8 @@ +package com.keylesspalace.tusky.util + +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding + +class BindingHolder<T : ViewBinding>( + val binding: T +) : RecyclerView.ViewHolder(binding.root) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BlurHashDecoder.kt b/app/src/main/java/com/keylesspalace/tusky/util/BlurHashDecoder.kt new file mode 100644 index 0000000..940f5f1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/BlurHashDecoder.kt @@ -0,0 +1,137 @@ +/** + * Blurhash implementation from blurhash project: + * https://github.com/woltapp/blurhash + * Minor modifications by charlag + * Major performance improvements by cbeyls + */ + +package com.keylesspalace.tusky.util + +import android.graphics.Bitmap +import android.graphics.Color +import kotlin.math.PI +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.withSign + +object BlurHashDecoder { + + fun decode(blurHash: String?, width: Int, height: Int, punch: Float = 1f): Bitmap? { + require(width > 0) { "Width must be greater than zero" } + require(height > 0) { "height must be greater than zero" } + if (blurHash == null || blurHash.length < 6) { + return null + } + val numCompEnc = decode83(blurHash, 0, 1) + val numCompX = (numCompEnc % 9) + 1 + val numCompY = (numCompEnc / 9) + 1 + val totalComp = numCompX * numCompY + if (blurHash.length != 4 + 2 * totalComp) { + return null + } + val maxAcEnc = decode83(blurHash, 1, 2) + val maxAc = (maxAcEnc + 1) / 166f + val colors = FloatArray(totalComp * 3) + var colorEnc = decode83(blurHash, 2, 6) + decodeDc(colorEnc, colors) + for (i in 1 until totalComp) { + val from = 4 + i * 2 + colorEnc = decode83(blurHash, from, from + 2) + decodeAc(colorEnc, maxAc * punch, colors, i * 3) + } + return composeBitmap(width, height, numCompX, numCompY, colors) + } + + private fun decode83(str: String, from: Int, to: Int): Int { + var result = 0 + for (i in from until to) { + val index = CHARS.indexOf(str[i]) + if (index != -1) { + result = result * 83 + index + } + } + return result + } + + private fun decodeDc(colorEnc: Int, outArray: FloatArray) { + val r = (colorEnc shr 16) and 0xFF + val g = (colorEnc shr 8) and 0xFF + val b = colorEnc and 0xFF + outArray[0] = srgbToLinear(r) + outArray[1] = srgbToLinear(g) + outArray[2] = srgbToLinear(b) + } + + private fun srgbToLinear(colorEnc: Int): Float { + val v = colorEnc / 255f + return if (v <= 0.04045f) { + (v / 12.92f) + } else { + ((v + 0.055f) / 1.055f).pow(2.4f) + } + } + + private fun decodeAc(value: Int, maxAc: Float, outArray: FloatArray, outIndex: Int) { + val r = value / (19 * 19) + val g = (value / 19) % 19 + val b = value % 19 + outArray[outIndex] = signedPow2((r - 9) / 9.0f) * maxAc + outArray[outIndex + 1] = signedPow2((g - 9) / 9.0f) * maxAc + outArray[outIndex + 2] = signedPow2((b - 9) / 9.0f) * maxAc + } + + private fun signedPow2(value: Float) = value.pow(2f).withSign(value) + + private fun composeBitmap( + width: Int, + height: Int, + numCompX: Int, + numCompY: Int, + colors: FloatArray + ): Bitmap { + val imageArray = IntArray(width * height) + val cosinesX = createCosines(width, numCompX) + val cosinesY = if (width == height && numCompX == numCompY) { + cosinesX + } else { + createCosines(height, numCompY) + } + for (y in 0 until height) { + for (x in 0 until width) { + var r = 0f + var g = 0f + var b = 0f + for (j in 0 until numCompY) { + val cosY = cosinesY[y * numCompY + j] + for (i in 0 until numCompX) { + val cosX = cosinesX[x * numCompX + i] + val basis = cosX * cosY + val colorIndex = (j * numCompX + i) * 3 + r += colors[colorIndex] * basis + g += colors[colorIndex + 1] * basis + b += colors[colorIndex + 2] * basis + } + } + imageArray[x + width * y] = Color.rgb(linearToSrgb(r), linearToSrgb(g), linearToSrgb(b)) + } + } + return Bitmap.createBitmap(imageArray, width, height, Bitmap.Config.ARGB_8888) + } + + private fun createCosines(size: Int, numComp: Int) = FloatArray(size * numComp) { index -> + val x = index / numComp + val i = index % numComp + cos(PI * x * i / size).toFloat() + } + + private fun linearToSrgb(value: Float): Int { + val v = value.coerceIn(0f, 1f) + return if (v <= 0.0031308f) { + (v * 12.92f * 255f + 0.5f).toInt() + } else { + ((1.055f * v.pow(1 / 2.4f) - 0.055f) * 255 + 0.5f).toInt() + } + } + + private const val CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/BundleExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/BundleExtensions.kt new file mode 100644 index 0000000..4aea349 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/BundleExtensions.kt @@ -0,0 +1,23 @@ +package com.keylesspalace.tusky.util + +import android.content.Intent +import android.os.Bundle +import android.os.Parcelable +import androidx.core.content.IntentCompat +import androidx.core.os.BundleCompat +import java.io.Serializable + +inline fun <reified T : Parcelable> Bundle.getParcelableCompat(key: String?): T? = + BundleCompat.getParcelable(this, key, T::class.java) + +inline fun <reified T : Serializable> Bundle.getSerializableCompat(key: String?): T? = + BundleCompat.getSerializable(this, key, T::class.java) + +inline fun <reified T : Parcelable> Intent.getParcelableExtraCompat(key: String?): T? = + IntentCompat.getParcelableExtra(this, key, T::class.java) + +inline fun <reified T : Serializable> Intent.getSerializableExtraCompat(key: String?): T? = + IntentCompat.getSerializableExtra(this, key, T::class.java) + +inline fun <reified T : Parcelable> Intent.getParcelableArrayListExtraCompat(key: String?): ArrayList<T>? = + IntentCompat.getParcelableArrayListExtra(this, key, T::class.java) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CardViewMode.kt b/app/src/main/java/com/keylesspalace/tusky/util/CardViewMode.kt new file mode 100644 index 0000000..81c2216 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/CardViewMode.kt @@ -0,0 +1,7 @@ +package com.keylesspalace.tusky.util + +enum class CardViewMode { + NONE, + FULL_WIDTH, + INDENTED +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CompositeWithOpaqueBackground.kt b/app/src/main/java/com/keylesspalace/tusky/util/CompositeWithOpaqueBackground.kt new file mode 100644 index 0000000..ded746e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/CompositeWithOpaqueBackground.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.util + +import android.graphics.Bitmap +import android.graphics.BitmapShader +import android.graphics.Canvas +import android.graphics.ColorMatrix +import android.graphics.ColorMatrixColorFilter +import android.graphics.Paint +import android.graphics.Shader +import com.bumptech.glide.load.Key +import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool +import com.bumptech.glide.load.resource.bitmap.BitmapTransformation +import com.bumptech.glide.util.Util +import java.nio.ByteBuffer +import java.security.MessageDigest + +/** + * Set an opaque background behind the non-transparent areas of a bitmap. + * + * Profile images may have areas that are partially transparent (i.e., alpha value >= 1 and < 255). + * + * Displaying those can be a problem if there is anything drawn under them, as it will show + * through the image. + * + * Fix this, by: + * + * - Creating a mask that matches the partially transparent areas of the image + * - Creating a new bitmap that, in the areas that match the mask, contains a background color + * - Composite the original image over the top + * + * So the partially transparent areas on the original image are composited over the original + * background, the fully transparent areas on the original image are left transparent. + */ +class CompositeWithOpaqueBackground(val backgroundColor: Int) : BitmapTransformation() { + + override fun equals(other: Any?): Boolean { + if (other is CompositeWithOpaqueBackground) { + return other.backgroundColor == backgroundColor + } + return false + } + + override fun hashCode() = Util.hashCode(ID.hashCode(), Util.hashCode(backgroundColor)) + + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update(ID_BYTES) + messageDigest.update(ByteBuffer.allocate(Int.SIZE_BYTES).putInt(backgroundColor).array()) + } + + override fun transform( + pool: BitmapPool, + toTransform: Bitmap, + outWidth: Int, + outHeight: Int + ): Bitmap { + // If the input bitmap has no alpha channel then there's nothing to do + if (!toTransform.hasAlpha()) return toTransform + + // Convert the background to a bitmap. + val backgroundBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ARGB_8888) + backgroundBitmap.eraseColor(backgroundColor) + + // Convert the alphaBitmap (where the alpha channel has 8bpp) to a mask of 1bpp + // TODO: toTransform.extractAlpha(paint, ...) could be used here, but I can't find any + // useful documentation covering paints and mask filters. + val maskBitmap = pool.get(outWidth, outHeight, Bitmap.Config.ALPHA_8).apply { + val canvas = Canvas(this) + canvas.drawBitmap(toTransform, 0f, 0f, EXTRACT_MASK_PAINT) + } + + val shader = BitmapShader(backgroundBitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT) + val paintShader = Paint() + paintShader.isAntiAlias = true + paintShader.shader = shader + paintShader.style = Paint.Style.FILL_AND_STROKE + + // Write the background to a new bitmap, masked to just the non-transparent areas of the + // original image + val dest = pool.get(outWidth, outHeight, toTransform.config) + val canvas = Canvas(dest) + canvas.drawBitmap(maskBitmap, 0f, 0f, paintShader) + + // Finally, write the original bitmap over the top + canvas.drawBitmap(toTransform, 0f, 0f, null) + + // Clean up intermediate bitmaps + pool.put(maskBitmap) + pool.put(backgroundBitmap) + + return dest + } + + companion object { + @Suppress("unused") + private const val TAG = "CompositeWithOpaqueBackground" + private val ID = CompositeWithOpaqueBackground::class.qualifiedName!! + private val ID_BYTES = ID.toByteArray(Key.CHARSET) + + /** Paint with a color filter that converts 8bpp alpha images to a 1bpp mask */ + private val EXTRACT_MASK_PAINT = Paint().apply { + colorFilter = ColorMatrixColorFilter( + ColorMatrix( + floatArrayOf( + 0f, 0f, 0f, 0f, 0f, + 0f, 0f, 0f, 0f, 0f, + 0f, 0f, 0f, 0f, 0f, + 0f, 0f, 0f, 255f, 0f + ) + ) + ) + isAntiAlias = false + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CryptoUtil.kt b/app/src/main/java/com/keylesspalace/tusky/util/CryptoUtil.kt new file mode 100644 index 0000000..d2fe7d5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/CryptoUtil.kt @@ -0,0 +1,60 @@ +/* Copyright 2022 Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import android.util.Base64 +import java.security.KeyPairGenerator +import java.security.SecureRandom +import java.security.Security +import org.bouncycastle.jce.ECNamedCurveTable +import org.bouncycastle.jce.interfaces.ECPrivateKey +import org.bouncycastle.jce.interfaces.ECPublicKey +import org.bouncycastle.jce.provider.BouncyCastleProvider + +object CryptoUtil { + const val CURVE_PRIME256_V1 = "prime256v1" + + private const val BASE64_FLAGS = Base64.URL_SAFE or Base64.NO_PADDING or Base64.NO_WRAP + + init { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME) + Security.addProvider(BouncyCastleProvider()) + } + + private fun secureRandomBytes(len: Int): ByteArray { + val ret = ByteArray(len) + SecureRandom.getInstance("SHA1PRNG").nextBytes(ret) + return ret + } + + fun secureRandomBytesEncoded(len: Int): String { + return Base64.encodeToString(secureRandomBytes(len), BASE64_FLAGS) + } + + data class EncodedKeyPair(val pubkey: String, val privKey: String) + + fun generateECKeyPair(curve: String): EncodedKeyPair { + val spec = ECNamedCurveTable.getParameterSpec(curve) + val gen = KeyPairGenerator.getInstance("EC", BouncyCastleProvider.PROVIDER_NAME) + gen.initialize(spec) + val keyPair = gen.genKeyPair() + val pubKey = keyPair.public as ECPublicKey + val privKey = keyPair.private as ECPrivateKey + val encodedPubKey = Base64.encodeToString(pubKey.q.getEncoded(false), BASE64_FLAGS) + val encodedPrivKey = Base64.encodeToString(privKey.d.toByteArray(), BASE64_FLAGS) + return EncodedKeyPair(encodedPubKey, encodedPrivKey) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt new file mode 100644 index 0000000..53290d0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomEmojiHelper.kt @@ -0,0 +1,235 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +@file:JvmName("CustomEmojiHelper") + +package com.keylesspalace.tusky.util + +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.Animatable +import android.graphics.drawable.Drawable +import android.text.style.ReplacementSpan +import android.view.View +import android.widget.TextView +import androidx.core.text.toSpannable +import com.bumptech.glide.Glide +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.target.Target +import com.bumptech.glide.request.transition.Transition +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Emoji + +/** + * replaces emoji shortcodes in a text with EmojiSpans + * @receiver the text containing custom emojis + * @param emojis a list of the custom emojis + * @param view a reference to the a view the emojis will be shown in (should be the TextView, but parents of the TextView are also acceptable) + * @return the text with the shortcodes replaced by EmojiSpans +*/ +fun CharSequence.emojify(emojis: List<Emoji>, view: View, animate: Boolean): CharSequence { + return view.updateEmojiTargets { + emojify(emojis, animate) + } +} + +class EmojiTargetScope<T : View>(val view: T) { + private val _targets = mutableListOf<Target<Drawable>>() + val targets: List<Target<Drawable>> + get() = _targets + + fun CharSequence.emojify(emojis: List<Emoji>, animate: Boolean): CharSequence { + if (emojis.isEmpty()) { + return this + } + + val spannable = toSpannable() + val requestManager = Glide.with(view) + + emojis.forEach { (shortcode, url, staticUrl) -> + val pattern = ":$shortcode:" + var start = indexOf(pattern) + + while (start != -1) { + val end = start + pattern.length + val span = EmojiSpan(view) + + spannable.setSpan(span, start, end, 0) + val target = span.createGlideTarget(view, animate) + requestManager + .asDrawable() + .load( + if (animate) { + url + } else { + staticUrl + } + ) + .into(target) + _targets.add(target) + + start = indexOf(pattern, end) + } + } + + return spannable + } +} + +inline fun <T : View, R> T.updateEmojiTargets(body: EmojiTargetScope<T>.() -> R): R { + clearEmojiTargets() + val scope = EmojiTargetScope(this) + val result = body(scope) + setEmojiTargets(scope.targets) + return result +} + +@Suppress("UNCHECKED_CAST") +fun View.clearEmojiTargets() { + getTag(R.id.custom_emoji_targets_tag)?.let { tag -> + val targets = tag as List<Target<Drawable>> + val requestManager = Glide.with(this) + targets.forEach { requestManager.clear(it) } + setTag(R.id.custom_emoji_targets_tag, null) + } +} + +fun View.setEmojiTargets(targets: List<Target<Drawable>>) { + setTag(R.id.custom_emoji_targets_tag, targets.takeIf { it.isNotEmpty() }) +} + +class EmojiSpan(view: View) : ReplacementSpan() { + + private val emojiSize: Int = if (view is TextView) { + view.paint.textSize + } else { + // sometimes it is not possible to determine the TextView the emoji will be shown in, + // e.g. because it is passed to a library, so we fallback to a size that should be large + // enough in most cases + view.context.resources.getDimension(R.dimen.fallback_emoji_size) + }.times(1.2).toInt() + + var imageDrawable: Drawable? = null + + override fun getSize( + paint: Paint, + text: CharSequence, + start: Int, + end: Int, + fm: Paint.FontMetricsInt? + ): Int { + if (fm != null) { + /* update FontMetricsInt or otherwise span does not get drawn when + * it covers the whole text */ + val metrics = paint.fontMetricsInt + fm.top = metrics.top + fm.ascent = metrics.ascent + fm.descent = metrics.descent + fm.bottom = metrics.bottom + } + + return emojiSize + } + + override fun draw( + canvas: Canvas, + text: CharSequence, + start: Int, + end: Int, + x: Float, + top: Int, + y: Int, + bottom: Int, + paint: Paint + ) { + imageDrawable?.let { drawable -> + canvas.save() + + // start with a width relative to the text size + var emojiWidth = paint.textSize * 1.1 + + // calculate the height, keeping the aspect ratio correct + val drawableWidth = drawable.intrinsicWidth + val drawableHeight = drawable.intrinsicHeight + var emojiHeight = emojiWidth / drawableWidth * drawableHeight + + // how much vertical space there is draw the emoji + val drawableSpace = (bottom - top).toDouble() + + // in case the calculated height is bigger than the available space, scale the emoji down, preserving aspect ratio + if (emojiHeight > drawableSpace) { + emojiWidth *= drawableSpace / emojiHeight + emojiHeight = drawableSpace + } + drawable.setBounds(0, 0, emojiWidth.toInt(), emojiHeight.toInt()) + + // vertically center the emoji in the line + val transY = top + (drawableSpace / 2 - emojiHeight / 2) + + canvas.translate(x, transY.toFloat()) + drawable.draw(canvas) + canvas.restore() + } + } + + fun createGlideTarget(view: View, animate: Boolean): Target<Drawable> { + return object : CustomTarget<Drawable>(emojiSize, emojiSize) { + override fun onStart() { + (imageDrawable as? Animatable)?.start() + } + + override fun onStop() { + (imageDrawable as? Animatable)?.stop() + } + + override fun onLoadFailed(errorDrawable: Drawable?) { + // Nothing to do + } + + override fun onResourceReady(resource: Drawable, transition: Transition<in Drawable>?) { + if (animate && resource is Animatable) { + resource.callback = object : Drawable.Callback { + override fun invalidateDrawable(who: Drawable) { + view.invalidate() + } + + override fun scheduleDrawable(who: Drawable, what: Runnable, `when`: Long) { + view.postDelayed(what, `when`) + } + + override fun unscheduleDrawable(who: Drawable, what: Runnable) { + view.removeCallbacks(what) + } + } + resource.start() + } + + imageDrawable = resource + view.invalidate() + } + + override fun onLoadCleared(placeholder: Drawable?) { + imageDrawable?.let { currentDrawable -> + if (currentDrawable is Animatable) { + currentDrawable.stop() + currentDrawable.callback = null + } + } + imageDrawable = null + view.invalidate() + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/CustomFragmentStateAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/util/CustomFragmentStateAdapter.kt new file mode 100644 index 0000000..eb31032 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/CustomFragmentStateAdapter.kt @@ -0,0 +1,28 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentActivity +import androidx.viewpager2.adapter.FragmentStateAdapter + +abstract class CustomFragmentStateAdapter( + private val activity: FragmentActivity +) : FragmentStateAdapter(activity) { + + fun getFragment(position: Int): Fragment? = + activity.supportFragmentManager.findFragmentByTag("f" + getItemId(position)) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/EmptyPagingSource.kt b/app/src/main/java/com/keylesspalace/tusky/util/EmptyPagingSource.kt new file mode 100644 index 0000000..d08a9c1 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/EmptyPagingSource.kt @@ -0,0 +1,14 @@ +package com.keylesspalace.tusky.util + +import androidx.paging.PagingSource +import androidx.paging.PagingState + +class EmptyPagingSource<T : Any> : PagingSource<Int, T>() { + override fun getRefreshKey(state: PagingState<Int, T>): Int? = null + + override suspend fun load(params: LoadParams<Int>): LoadResult<Int, T> = LoadResult.Page( + emptyList(), + null, + null + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/FocalPointUtil.kt b/app/src/main/java/com/keylesspalace/tusky/util/FocalPointUtil.kt new file mode 100644 index 0000000..28bfdd6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/FocalPointUtil.kt @@ -0,0 +1,165 @@ +/* Copyright 2018 Jochem Raat <jchmrt@riseup.net> + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import android.graphics.Matrix +import com.keylesspalace.tusky.entity.Attachment.Focus + +/** + * Calculates the image matrix needed to maintain the correct cropping for image views based on + * their focal point. + * + * The purpose of this class is to make sure that the focal point information on media + * attachments are honoured. This class uses the custom matrix option of android ImageView's to + * customize how the image is cropped into the view. + * + * See the explanation of focal points here: + * https://github.com/jonom/jquery-focuspoint#1-calculate-your-images-focus-point + */ +object FocalPointUtil { + /** + * Update the given matrix for the given parameters. + * + * How it works is using the following steps: + * - First we determine if the image is too wide or too tall for the view size. If it is + * too wide, we need to crop it horizontally and scale the height to fit the view + * exactly. If it is too tall we need to crop vertically and scale the width to fit the + * view exactly. + * - Then we determine what translation is needed to get the focal point in view. We + * prefer to get the focal point at the center of the preview. However if that would + * result in some part of the preview being empty, we instead align the image so that it + * fills the view, but still the focal point is always in view. + * + * @param viewWidth The width of the imageView. + * @param viewHeight The height of the imageView + * @param imageWidth The width of the actual image + * @param imageHeight The height of the actual image + * @param focus The focal point to focus + * @param mat The matrix to update, this matrix is reset() and then updated with the new + * configuration. We reuse the old matrix to prevent unnecessary allocations. + * + * @return The matrix which correctly crops the image + */ + fun updateFocalPointMatrix( + viewWidth: Float, + viewHeight: Float, + imageWidth: Float, + imageHeight: Float, + focus: Focus, + mat: Matrix + ) { + // Reset the cached matrix: + mat.reset() + + // calculate scaling: + val scale = calculateScaling(viewWidth, viewHeight, imageWidth, imageHeight) + mat.preScale(scale, scale) + + // calculate offsets: + var top = 0f + var left = 0f + if (isVerticalCrop(viewWidth, viewHeight, imageWidth, imageHeight)) { + top = focalOffset(viewHeight, imageHeight, scale, focalYToCoordinate(focus.y ?: 0f)) + } else { // horizontal crop + left = focalOffset(viewWidth, imageWidth, scale, focalXToCoordinate(focus.x ?: 0f)) + } + + mat.postTranslate(left, top) + } + + /** + * Calculate the scaling of the image needed to make it fill the screen. + * + * The scaling used depends on if we need a vertical of horizontal crop. + */ + fun calculateScaling( + viewWidth: Float, + viewHeight: Float, + imageWidth: Float, + imageHeight: Float + ): Float { + return if (isVerticalCrop(viewWidth, viewHeight, imageWidth, imageHeight)) { + viewWidth / imageWidth + } else { // horizontal crop: + viewHeight / imageHeight + } + } + + /** + * Return true if we need a vertical crop, false for a horizontal crop. + */ + fun isVerticalCrop( + viewWidth: Float, + viewHeight: Float, + imageWidth: Float, + imageHeight: Float + ): Boolean { + val viewRatio = viewWidth / viewHeight + val imageRatio = imageWidth / imageHeight + + return viewRatio > imageRatio + } + + /** + * Transform the focal x component to the corresponding coordinate on the image. + * + * This means that we go from a representation where the left side of the image is -1 and + * the right side +1, to a representation with the left side being 0 and the right side + * being +1. + */ + fun focalXToCoordinate(x: Float): Float { + return (x + 1) / 2 + } + + /** + * Transform the focal y component to the corresponding coordinate on the image. + * + * This means that we go from a representation where the bottom side of the image is -1 and + * the top side +1, to a representation with the top side being 0 and the bottom side + * being +1. + */ + fun focalYToCoordinate(y: Float): Float { + return (-y + 1) / 2 + } + + /** + * Calculate the relative offset needed to focus on the focal point in one direction. + * + * This method works for both the vertical and horizontal crops. It simply calculates + * what offset to take based on the proportions between the scaled image and the view + * available. It also makes sure to always fill the bounds of the view completely with + * the image. So it won't put the very edge of the image in center, because that would + * leave part of the view empty. + */ + fun focalOffset(view: Float, image: Float, scale: Float, focal: Float): Float { + // The fraction of the image that will be in view: + val inView = view / (scale * image) + var offset = 0f + + // These values indicate the maximum and minimum focal parameter possible while still + // keeping the entire view filled with the image: + val maxFocal = 1 - inView / 2 + val minFocal = inView / 2 + + if (focal > maxFocal) { + offset = -((2 - inView) / 2) * image * scale + view * 0.5f + } else if (focal > minFocal) { + offset = -focal * image * scale + view * 0.5f + } + + return offset + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/GlideExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/GlideExtensions.kt new file mode 100644 index 0000000..01629c7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/GlideExtensions.kt @@ -0,0 +1,48 @@ +package com.keylesspalace.tusky.util + +import com.bumptech.glide.RequestBuilder +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException +import kotlinx.coroutines.suspendCancellableCoroutine + +/** + * Allows waiting for a Glide request to complete without blocking a background thread. + */ +suspend fun <R> RequestBuilder<R>.submitAsync( + width: Int = Target.SIZE_ORIGINAL, + height: Int = Target.SIZE_ORIGINAL +): R { + return suspendCancellableCoroutine { continuation -> + val target = addListener( + object : RequestListener<R> { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target<R>, + isFirstResource: Boolean + ): Boolean { + continuation.resumeWithException(e ?: GlideException("Image loading failed")) + return false + } + + override fun onResourceReady( + resource: R & Any, + model: Any, + target: Target<R>?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + if (target?.request?.isComplete == true) { + continuation.resume(resource) + } + return false + } + } + ).submit(width, height) + continuation.invokeOnCancellation { target.cancel(true) } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/GlideModule.kt b/app/src/main/java/com/keylesspalace/tusky/util/GlideModule.kt new file mode 100644 index 0000000..774c860 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/GlideModule.kt @@ -0,0 +1,7 @@ +package com.keylesspalace.tusky.util + +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.AppGlideModule + +@GlideModule +class GlideModule : AppGlideModule() diff --git a/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.kt b/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.kt new file mode 100644 index 0000000..8b902b5 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/HttpHeaderLink.kt @@ -0,0 +1,134 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ +package com.keylesspalace.tusky.util + +import android.net.Uri +import androidx.annotation.VisibleForTesting +import androidx.core.net.toUri + +/** + * Represents one link and its parameters from the link header of an HTTP message. + * + * @see [RFC5988](https://tools.ietf.org/html/rfc5988) + */ +class HttpHeaderLink +@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE) +constructor( + uri: String +) { + data class Parameter(val name: String, val value: String?) + + private val parameters: MutableList<Parameter> = ArrayList() + + val uri: Uri = uri.toUri() + + private data class ValueResult(val value: String, val end: Int = -1) + + companion object { + private fun findEndOfQuotedString(line: String, start: Int): Int { + var i = start + while (i < line.length) { + val c = line[i] + if (c == '\\') { + i += 1 + } else if (c == '"') { + return i + } + i++ + } + return -1 + } + + private fun parseValue(line: String, start: Int): ValueResult { + val foundIndex = line.indexOfAny(charArrayOf(';', ',', '"'), start, false) + if (foundIndex == -1) { + return ValueResult(line.substring(start).trim()) + } + val c = line[foundIndex] + return if (c == ';' || c == ',') { + ValueResult(line.substring(start, foundIndex).trim(), foundIndex) + } else { + var quoteEnd = findEndOfQuotedString(line, foundIndex + 1) + if (quoteEnd == -1) { + quoteEnd = line.length + } + ValueResult(line.substring(foundIndex + 1, quoteEnd).trim(), quoteEnd) + } + } + + private fun parseParameters(line: String, start: Int, link: HttpHeaderLink): Int { + var i = start + while (i < line.length) { + val foundIndex = line.indexOfAny(charArrayOf('=', ','), i, false) + if (foundIndex == -1) { + return -1 + } else if (line[foundIndex] == ',') { + return foundIndex + } + val name = line.substring(line.indexOf(';', i) + 1, foundIndex).trim() + val result = parseValue(line, foundIndex) + val value = result.value + val parameter = Parameter(name, value) + link.parameters.add(parameter) + i = if (result.end == -1) { + return -1 + } else { + result.end + } + } + return -1 + } + + /** + * @param line the entire link header, not including the initial "Link:" + * @return all links found in the header + */ + fun parse(line: String?): List<HttpHeaderLink> { + val links: MutableList<HttpHeaderLink> = mutableListOf() + line ?: return links + + var i = 0 + while (i < line.length) { + val uriEnd = line.indexOf('>', i) + val uri = line.substring(line.indexOf('<', i) + 1, uriEnd) + val link = HttpHeaderLink(uri) + links.add(link) + val parseEnd = parseParameters(line, uriEnd, link) + i = if (parseEnd == -1) { + break + } else { + parseEnd + } + i++ + } + + return links + } + + /** + * @param links intended to be those returned by parse() + * @param relationType of the parameter "rel", commonly "next" or "prev" + * @return the link matching the given relation type + */ + fun findByRelationType(links: List<HttpHeaderLink>, relationType: String): HttpHeaderLink? { + return links.find { link -> + link.parameters.any { parameter -> + parameter.name == "rel" && parameter.value == relationType + } + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.kt new file mode 100644 index 0000000..f17e970 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/IOUtils.kt @@ -0,0 +1,47 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import android.content.ContentResolver +import android.net.Uri +import java.io.Closeable +import java.io.File +import java.io.IOException +import okio.buffer +import okio.sink +import okio.source + +fun Closeable.closeQuietly() { + try { + close() + } catch (e: IOException) { + // intentionally unhandled + } +} + +fun Uri.copyToFile(contentResolver: ContentResolver, file: File): Boolean { + return try { + val inputStream = contentResolver.openInputStream(this) ?: return false + inputStream.source().use { source -> + file.sink().buffer().use { bufferedSink -> + bufferedSink.writeAll(source) + } + } + true + } catch (e: IOException) { + false + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/IconUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/IconUtils.kt new file mode 100644 index 0000000..3cb34fe --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/IconUtils.kt @@ -0,0 +1,33 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import android.content.Context +import android.graphics.Color +import androidx.annotation.Px +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.R +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import com.mikepenz.iconics.utils.colorInt +import com.mikepenz.iconics.utils.sizePx + +fun makeIcon(context: Context, icon: GoogleMaterial.Icon, @Px iconSize: Int): IconicsDrawable { + return IconicsDrawable(context, icon).apply { + sizePx = iconSize + colorInt = MaterialColors.getColor(context, R.attr.iconColor, Color.BLACK) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt new file mode 100644 index 0000000..0979f59 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ImageLoadingHelper.kt @@ -0,0 +1,58 @@ +@file:JvmName("ImageLoadingHelper") + +package com.keylesspalace.tusky.util + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.widget.ImageView +import androidx.annotation.Px +import com.bumptech.glide.Glide +import com.bumptech.glide.load.MultiTransformation +import com.bumptech.glide.load.Transformation +import com.bumptech.glide.load.resource.bitmap.CenterCrop +import com.bumptech.glide.load.resource.bitmap.RoundedCorners +import com.keylesspalace.tusky.R + +private val centerCropTransformation = CenterCrop() + +fun loadAvatar( + url: String?, + imageView: ImageView, + @Px radius: Int, + animate: Boolean, + transforms: List<Transformation<Bitmap>>? = null +) { + if (url.isNullOrBlank()) { + Glide.with(imageView) + .load(R.drawable.avatar_default) + .into(imageView) + } else { + val multiTransformation = MultiTransformation( + buildList { + transforms?.let { this.addAll(it) } + add(centerCropTransformation) + add(RoundedCorners(radius)) + } + ) + + if (animate) { + Glide.with(imageView) + .load(url) + .transform(multiTransformation) + .placeholder(R.drawable.avatar_default) + .into(imageView) + } else { + Glide.with(imageView) + .asBitmap() + .load(url) + .transform(multiTransformation) + .placeholder(R.drawable.avatar_default) + .into(imageView) + } + } +} + +fun decodeBlurHash(context: Context, blurhash: String): BitmapDrawable { + return BitmapDrawable(context.resources, BlurHashDecoder.decode(blurhash, 32, 32, 1f)) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/Lazy.kt b/app/src/main/java/com/keylesspalace/tusky/util/Lazy.kt new file mode 100644 index 0000000..8744556 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/Lazy.kt @@ -0,0 +1,5 @@ +package com.keylesspalace.tusky.util + +@Suppress("NOTHING_TO_INLINE") +inline fun <T : Any> unsafeLazy(noinline initializer: () -> T): Lazy<T> = + lazy(LazyThreadSafetyMode.NONE, initializer) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LifecycleExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/LifecycleExtensions.kt new file mode 100644 index 0000000..c0d99b6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/LifecycleExtensions.kt @@ -0,0 +1,21 @@ +package com.keylesspalace.tusky.util + +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.coroutineScope +import androidx.lifecycle.repeatOnLifecycle +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch + +fun Lifecycle.launchAndRepeatOnLifecycle( + state: Lifecycle.State = Lifecycle.State.STARTED, + block: suspend CoroutineScope.() -> Unit +): Job = coroutineScope.launch { + repeatOnLifecycle(state, block) +} + +fun LifecycleOwner.launchAndRepeatOnLifecycle( + state: Lifecycle.State = Lifecycle.State.STARTED, + block: suspend CoroutineScope.() -> Unit +): Job = lifecycle.launchAndRepeatOnLifecycle(state, block) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt new file mode 100644 index 0000000..239b196 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/LinkHelper.kt @@ -0,0 +1,446 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ +@file:JvmName("LinkHelper") + +package com.keylesspalace.tusky.util + +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.graphics.Color +import android.net.Uri +import android.os.Build +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.method.LinkMovementMethod +import android.text.style.ClickableSpan +import android.text.style.ForegroundColorSpan +import android.text.style.QuoteSpan +import android.text.style.URLSpan +import android.util.Log +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_UP +import android.view.View +import android.widget.TextView +import androidx.annotation.VisibleForTesting +import androidx.appcompat.content.res.AppCompatResources +import androidx.browser.customtabs.CustomTabColorSchemeParams +import androidx.browser.customtabs.CustomTabsIntent +import androidx.core.net.toUri +import androidx.preference.PreferenceManager +import at.connyduck.sparkbutton.helpers.Utils +import com.google.android.material.R as materialR +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.HashTag +import com.keylesspalace.tusky.entity.Status.Mention +import com.keylesspalace.tusky.interfaces.LinkListener +import com.keylesspalace.tusky.settings.PrefKeys +import java.net.URI +import java.net.URISyntaxException + +fun getDomain(urlString: String?): String { + val host = urlString?.toUri()?.host + return when { + host == null -> "" + host.startsWith("www.") -> host.substring(4) + else -> host + } +} + +/** + * Finds links, mentions, and hashtags in a piece of text and makes them clickable, associating + * them with callbacks to notify when they're clicked. + * + * @param view the returned text will be put in + * @param content containing text with mentions, links, or hashtags + * @param mentions any '@' mentions which are known to be in the content + * @param listener to notify about particular spans that are clicked + */ +fun setClickableText( + view: TextView, + content: CharSequence, + mentions: List<Mention>, + tags: List<HashTag>?, + listener: LinkListener +) { + val spannableContent = markupHiddenUrls(view, content) + + view.text = spannableContent.apply { + styleQuoteSpans(view) + getSpans(0, spannableContent.length, URLSpan::class.java).forEach { span -> + setClickableText(span, this, mentions, tags, listener) + } + } + view.movementMethod = NoTrailingSpaceLinkMovementMethod +} + +@VisibleForTesting +fun markupHiddenUrls(view: TextView, content: CharSequence): SpannableStringBuilder { + val spannableContent = SpannableStringBuilder(content) + val originalSpans = spannableContent.getSpans(0, content.length, URLSpan::class.java) + val obscuredLinkSpans = originalSpans.filter { + val start = spannableContent.getSpanStart(it) + val firstCharacter = content[start] + return@filter if (firstCharacter == '#' || firstCharacter == '@') { + false + } else { + val text = spannableContent.subSequence( + start, + spannableContent.getSpanEnd(it) + ).toString() + .split(' ').lastOrNull().orEmpty() + var textDomain = getDomain(text) + if (textDomain.isBlank()) { + textDomain = getDomain("https://$text") + } + getDomain(it.url) != textDomain + } + } + + for (span in obscuredLinkSpans) { + val start = spannableContent.getSpanStart(span) + val end = spannableContent.getSpanEnd(span) + val originalText = spannableContent.subSequence(start, end) + val replacementText = view.context.getString( + R.string.url_domain_notifier, + originalText, + getDomain(span.url) + ) + spannableContent.replace( + start, + end, + replacementText + ) // this also updates the span locations + + val linkDrawable = AppCompatResources.getDrawable(view.context, R.drawable.ic_link)!! + // ImageSpan does not always align the icon correctly in the line, let's use our custom emoji span for this + val linkDrawableSpan = EmojiSpan(view) + linkDrawableSpan.imageDrawable = linkDrawable + + val placeholderIndex = replacementText.indexOf("🔗") + + spannableContent.setSpan( + linkDrawableSpan, + start + placeholderIndex, + start + placeholderIndex + "🔗".length, + 0 + ) + } + + return spannableContent +} + +@VisibleForTesting +fun setClickableText( + span: URLSpan, + builder: SpannableStringBuilder, + mentions: List<Mention>, + tags: List<HashTag>?, + listener: LinkListener +) = builder.apply { + val start = getSpanStart(span) + val end = getSpanEnd(span) + val flags = getSpanFlags(span) + val text = subSequence(start, end) + + val customSpan = when (text[0]) { + '#' -> getCustomSpanForTag(text, tags, span, listener) + '@' -> getCustomSpanForMention(mentions, span, listener) + else -> null + } ?: object : NoUnderlineURLSpan(span.url) { + override fun onClick(view: View) = listener.onViewUrl(url) + } + + removeSpan(span) + setSpan(customSpan, start, end, flags) +} + +@VisibleForTesting +fun getTagName(text: CharSequence, tags: List<HashTag>?): String? { + val scrapedName = normalizeToASCII(text.subSequence(1, text.length)) + return when (tags) { + null -> scrapedName + else -> tags.firstOrNull { it.name.equals(scrapedName, true) }?.name + } +} + +private fun getCustomSpanForTag( + text: CharSequence, + tags: List<HashTag>?, + span: URLSpan, + listener: LinkListener +): ClickableSpan? { + return getTagName(text, tags)?.let { + object : NoUnderlineURLSpan(span.url) { + override fun onClick(view: View) = listener.onViewTag(it) + } + } +} + +private fun getCustomSpanForMention( + mentions: List<Mention>, + span: URLSpan, + listener: LinkListener +): ClickableSpan? { + // https://github.com/tuskyapp/Tusky/pull/2339 + return mentions.firstOrNull { it.url == span.url }?.let { + getCustomSpanForMentionUrl(span.url, it.id, listener) + } +} + +private fun getCustomSpanForMentionUrl( + url: String, + mentionId: String, + listener: LinkListener +): ClickableSpan { + return object : MentionSpan(url) { + override fun onClick(view: View) = listener.onViewAccount(mentionId) + } +} + +private fun SpannableStringBuilder.styleQuoteSpans(view: TextView) { + getSpans(0, length, QuoteSpan::class.java).forEach { span -> + val start = getSpanStart(span) + val end = getSpanEnd(span) + val flags = getSpanFlags(span) + + val quoteColor = MaterialColors.getColor(view, android.R.attr.textColorTertiary) + + val newQuoteSpan = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + QuoteSpan( + quoteColor, + Utils.dpToPx(view.context, 3), + Utils.dpToPx(view.context, 8) + ) + } else { + QuoteSpan(quoteColor) + } + + val quoteColorSpan = ForegroundColorSpan(quoteColor) + + removeSpan(span) + setSpan(newQuoteSpan, start, end, flags) + setSpan(quoteColorSpan, start, end, flags) + } +} + +/** + * Put mentions in a piece of text and makes them clickable, associating them with callbacks to + * notify when they're clicked. + * + * @param view the returned text will be put in + * @param mentions any '@' mentions which are known to be in the content + * @param listener to notify about particular spans that are clicked + */ +fun setClickableMentions(view: TextView, mentions: List<Mention>?, listener: LinkListener) { + if (mentions?.isEmpty() != false) { + view.text = null + return + } + + view.text = SpannableStringBuilder().apply { + var start = 0 + var end = 0 + var flags: Int + var firstMention = true + + for (mention in mentions) { + val customSpan = getCustomSpanForMentionUrl(mention.url, mention.id, listener) + end += 1 + mention.localUsername.length // length of @ + username + flags = getSpanFlags(customSpan) + if (firstMention) { + firstMention = false + } else { + append(" ") + start += 1 + end += 1 + } + + append("@") + append(mention.localUsername) + setSpan(customSpan, start, end, flags) + start = end + } + } + view.movementMethod = NoTrailingSpaceLinkMovementMethod +} + +fun createClickableText(text: String, link: String): CharSequence { + return SpannableStringBuilder(text).apply { + setSpan(NoUnderlineURLSpan(link), 0, text.length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE) + } +} + +/** + * Opens a link, depending on the settings, either in the browser or in a custom tab + * + * @receiver the Context to open the link from + * @param url a string containing the url to open + */ +fun Context.openLink(url: String) { + val uri = url.toUri().normalizeScheme() + val useCustomTabs = PreferenceManager.getDefaultSharedPreferences( + this + ).getBoolean(PrefKeys.CUSTOM_TABS, false) + + if (useCustomTabs) { + openLinkInCustomTab(uri, this) + } else { + openLinkInBrowser(uri, this) + } +} + +/** + * opens a link in the browser via Intent.ACTION_VIEW + * + * @param uri the uri to open + * @param context context + */ +private fun openLinkInBrowser(uri: Uri?, context: Context) { + val intent = Intent(Intent.ACTION_VIEW, uri) + try { + context.startActivity(intent) + } catch (e: ActivityNotFoundException) { + Log.w(TAG, "Activity was not found for intent, $intent") + } +} + +/** + * tries to open a link in a custom tab + * falls back to browser if not possible + * + * @param uri the uri to open + * @param context context + */ +fun openLinkInCustomTab(uri: Uri, context: Context) { + val toolbarColor = MaterialColors.getColor( + context, + materialR.attr.colorSurface, + Color.BLACK + ) + val navigationbarColor = MaterialColors.getColor( + context, + android.R.attr.navigationBarColor, + Color.BLACK + ) + val navigationbarDividerColor = MaterialColors.getColor( + context, + R.attr.dividerColor, + Color.BLACK + ) + val colorSchemeParams = CustomTabColorSchemeParams.Builder() + .setToolbarColor(toolbarColor) + .setNavigationBarColor(navigationbarColor) + .setNavigationBarDividerColor(navigationbarDividerColor) + .build() + val customTabsIntent = CustomTabsIntent.Builder() + .setDefaultColorSchemeParams(colorSchemeParams) + .setShareState(CustomTabsIntent.SHARE_STATE_ON) + .setShowTitle(true) + .build() + + try { + customTabsIntent.launchUrl(context, uri) + } catch (e: ActivityNotFoundException) { + Log.w(TAG, "Activity was not found for intent $customTabsIntent") + openLinkInBrowser(uri, context) + } +} + +// https://mastodon.foo.bar/@User +// https://mastodon.foo.bar/@User/43456787654678 +// https://mastodon.foo.bar/users/User/statuses/43456787654678 +// https://pleroma.foo.bar/users/User +// https://pleroma.foo.bar/users/9qTHT2ANWUdXzENqC0 +// https://pleroma.foo.bar/notice/9sBHWIlwwGZi5QGlHc +// https://pleroma.foo.bar/objects/d4643c42-3ae0-4b73-b8b0-c725f5819207 +// https://friendica.foo.bar/profile/user +// https://friendica.foo.bar/display/d4643c42-3ae0-4b73-b8b0-c725f5819207 +// https://misskey.foo.bar/notes/83w6r388br (always lowercase) +// https://pixelfed.social/p/connyduck/391263492998670833 +// https://pixelfed.social/connyduck +// https://gts.foo.bar/@goblin/statuses/01GH9XANCJ0TA8Y95VE9H3Y0Q2 +// https://gts.foo.bar/@goblin +// https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5 +// https://bookwyrm.foo.bar/user/User +// https://bookwyrm.foo.bar/user/User/comment/123456 +fun looksLikeMastodonUrl(urlString: String): Boolean { + val uri: URI + try { + uri = URI(urlString) + } catch (e: URISyntaxException) { + return false + } + + if (uri.query != null || + uri.fragment != null || + uri.path == null + ) { + return false + } + + return uri.path.let { + it.matches("^/@[^/]+$".toRegex()) || + it.matches("^/@[^/]+/\\d+$".toRegex()) || + it.matches("^/users/[^/]+/statuses/\\d+$".toRegex()) || + it.matches("^/users/\\w+$".toRegex()) || + it.matches("^/user/[^/]+/comment/\\d+$".toRegex()) || + it.matches("^/user/\\w+$".toRegex()) || + it.matches("^/notice/[a-zA-Z0-9]+$".toRegex()) || + it.matches("^/objects/[-a-f0-9]+$".toRegex()) || + it.matches("^/notes/[a-z0-9]+$".toRegex()) || + it.matches("^/display/[-a-f0-9]+$".toRegex()) || + it.matches("^/profile/\\w+$".toRegex()) || + it.matches("^/p/\\w+/\\d+$".toRegex()) || + it.matches("^/\\w+$".toRegex()) || + it.matches("^/@[^/]+/statuses/[a-zA-Z0-9]+$".toRegex()) || + it.matches("^/o/[a-f0-9]+$".toRegex()) + } +} + +private const val TAG = "LinkHelper" + +/** + * [LinkMovementMethod] that doesn't add a leading/trailing clickable area. + * + * [LinkMovementMethod] has a bug in its calculation of the clickable width of a span on a line. If + * the span is the last thing on the line the clickable area extends to the end of the view. So the + * user can tap what appears to be whitespace and open a link. + * + * Fix this by overriding ACTION_UP touch events and calculating the true start and end of the + * content on the line that was tapped. Then ignore clicks that are outside this area. + * + * See https://github.com/tuskyapp/Tusky/issues/1567. + */ +object NoTrailingSpaceLinkMovementMethod : LinkMovementMethod() { + override fun onTouchEvent(widget: TextView, buffer: Spannable, event: MotionEvent): Boolean { + val action = event.action + if (action != ACTION_UP) return super.onTouchEvent(widget, buffer, event) + + val x = event.x.toInt() + val y = event.y.toInt() - widget.totalPaddingTop + widget.scrollY + val line = widget.layout.getLineForVertical(y) + val lineLeft = widget.layout.getLineLeft(line) + val lineRight = widget.layout.getLineRight(line) + if (x > lineRight || x >= 0 && x < lineLeft) { + return true + } + + return super.onTouchEvent(widget, buffer, event) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt new file mode 100644 index 0000000..537c7ac --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListStatusAccessibilityDelegate.kt @@ -0,0 +1,359 @@ +package com.keylesspalace.tusky.util + +import android.content.Context +import android.os.Bundle +import android.text.Spannable +import android.text.style.URLSpan +import android.view.View +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityManager +import android.widget.ArrayAdapter +import androidx.appcompat.app.AlertDialog +import androidx.core.view.AccessibilityDelegateCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.adapter.StatusBaseViewHolder +import com.keylesspalace.tusky.entity.Status.Companion.MAX_MEDIA_ATTACHMENTS +import com.keylesspalace.tusky.interfaces.StatusActionListener +import com.keylesspalace.tusky.viewdata.StatusViewData +import kotlin.math.min + +// Not using lambdas because there's boxing of int then +fun interface StatusProvider { + fun getStatus(pos: Int): StatusViewData? +} + +class ListStatusAccessibilityDelegate( + private val recyclerView: RecyclerView, + private val statusActionListener: StatusActionListener, + private val statusProvider: StatusProvider +) : RecyclerViewAccessibilityDelegate(recyclerView) { + private val a11yManager = context.getSystemService(Context.ACCESSIBILITY_SERVICE) + as AccessibilityManager + + override fun getItemDelegate(): AccessibilityDelegateCompat = itemDelegate + + private val context: Context get() = recyclerView.context + + private val itemDelegate = object : ItemDelegate(this) { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfoCompat + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + + val pos = recyclerView.getChildAdapterPosition(host) + val status = statusProvider.getStatus(pos) ?: return + if (status is StatusViewData.Concrete) { + if (status.status.spoilerText.isNotEmpty()) { + info.addAction(if (status.isExpanded) collapseCwAction else expandCwAction) + } + + info.addAction(replyAction) + + val actionable = status.actionable + if (actionable.isRebloggingAllowed) { + info.addAction(if (actionable.reblogged) unreblogAction else reblogAction) + } + info.addAction(if (actionable.favourited) unfavouriteAction else favouriteAction) + info.addAction(if (actionable.bookmarked) unbookmarkAction else bookmarkAction) + + val mediaActions = intArrayOf( + R.id.action_open_media_1, + R.id.action_open_media_2, + R.id.action_open_media_3, + R.id.action_open_media_4 + ) + val attachmentCount = min(actionable.attachments.size, MAX_MEDIA_ATTACHMENTS) + for (i in 0 until attachmentCount) { + info.addAction( + AccessibilityActionCompat( + mediaActions[i], + context.getString(R.string.action_open_media_n, i + 1) + ) + ) + } + + info.addAction(openProfileAction) + if (getLinks(status).any()) info.addAction(linksAction) + + val mentions = actionable.mentions + if (mentions.isNotEmpty()) info.addAction(mentionsAction) + + if (getHashtags(status).any()) info.addAction(hashtagsAction) + if (!status.status.reblog?.account?.username.isNullOrEmpty()) { + info.addAction(openRebloggerAction) + } + if (actionable.reblogsCount > 0) info.addAction(openRebloggedByAction) + if (actionable.favouritesCount > 0) info.addAction(openFavsAction) + + info.addAction(moreAction) + } + } + + override fun performAccessibilityAction(host: View, action: Int, args: Bundle?): Boolean { + val pos = recyclerView.getChildAdapterPosition(host) + when (action) { + R.id.action_reply -> { + interrupt() + statusActionListener.onReply(pos) + } + R.id.action_favourite -> statusActionListener.onFavourite(true, pos) + R.id.action_unfavourite -> statusActionListener.onFavourite(false, pos) + R.id.action_bookmark -> statusActionListener.onBookmark(true, pos) + R.id.action_unbookmark -> statusActionListener.onBookmark(false, pos) + R.id.action_reblog -> statusActionListener.onReblog(true, pos) + R.id.action_unreblog -> statusActionListener.onReblog(false, pos) + R.id.action_open_profile -> { + interrupt() + statusActionListener.onViewAccount( + ( + statusProvider.getStatus( + pos + ) as StatusViewData.Concrete + ).actionable.account.id + ) + } + R.id.action_open_media_1 -> { + interrupt() + statusActionListener.onViewMedia(pos, 0, null) + } + R.id.action_open_media_2 -> { + interrupt() + statusActionListener.onViewMedia(pos, 1, null) + } + R.id.action_open_media_3 -> { + interrupt() + statusActionListener.onViewMedia(pos, 2, null) + } + R.id.action_open_media_4 -> { + interrupt() + statusActionListener.onViewMedia(pos, 3, null) + } + R.id.action_expand_cw -> { + // Toggling it directly to avoid animations + // which cannot be disabled for detailed status for some reason + val holder = recyclerView.getChildViewHolder(host) as StatusBaseViewHolder + holder.toggleContentWarning() + // Stop and restart narrator before it reads old description. + // Would be nice if we could *just* read the content here but doesn't seem + // to be possible. + forceFocus(host) + } + R.id.action_collapse_cw -> { + statusActionListener.onExpandedChange(false, pos) + interrupt() + } + R.id.action_links -> showLinksDialog(host) + R.id.action_mentions -> showMentionsDialog(host) + R.id.action_hashtags -> showHashtagsDialog(host) + R.id.action_open_reblogger -> { + interrupt() + statusActionListener.onOpenReblog(pos) + } + R.id.action_open_reblogged_by -> { + interrupt() + statusActionListener.onShowReblogs(pos) + } + R.id.action_open_faved_by -> { + interrupt() + statusActionListener.onShowFavs(pos) + } + R.id.action_more -> { + statusActionListener.onMore(host, pos) + } + else -> return super.performAccessibilityAction(host, action, args) + } + return true + } + + private fun showLinksDialog(host: View) { + val status = getStatus(host) as? StatusViewData.Concrete ?: return + val links = getLinks(status).toList() + val textLinks = links.map { item -> item.link } + AlertDialog.Builder(host.context) + .setTitle(R.string.title_links_dialog) + .setAdapter( + ArrayAdapter( + host.context, + android.R.layout.simple_list_item_1, + textLinks + ) + ) { _, which -> host.context.openLink(links[which].link) } + .show() + .let { forceFocus(it.listView) } + } + + private fun showMentionsDialog(host: View) { + val status = getStatus(host) as? StatusViewData.Concrete ?: return + val mentions = status.actionable.mentions + val stringMentions = mentions.map { it.username } + AlertDialog.Builder(host.context) + .setTitle(R.string.title_mentions_dialog) + .setAdapter( + ArrayAdapter<CharSequence>( + host.context, + android.R.layout.simple_list_item_1, + stringMentions + ) + ) { _, which -> + statusActionListener.onViewAccount(mentions[which].id) + } + .show() + .let { forceFocus(it.listView) } + } + + private fun showHashtagsDialog(host: View) { + val status = getStatus(host) as? StatusViewData.Concrete ?: return + val tags = getHashtags(status).map { it.subSequence(1, it.length) }.toList() + AlertDialog.Builder(host.context) + .setTitle(R.string.title_hashtags_dialog) + .setAdapter( + ArrayAdapter( + host.context, + android.R.layout.simple_list_item_1, + tags + ) + ) { _, which -> + statusActionListener.onViewTag(tags[which].toString()) + } + .show() + .let { forceFocus(it.listView) } + } + + private fun getStatus(childView: View): StatusViewData { + return statusProvider.getStatus(recyclerView.getChildAdapterPosition(childView))!! + } + } + + private fun getLinks(status: StatusViewData.Concrete): Sequence<LinkSpanInfo> { + val content = status.content + return if (content is Spannable) { + content.getSpans(0, content.length, URLSpan::class.java) + .asSequence() + .map { span -> + val text = content.subSequence( + content.getSpanStart(span), + content.getSpanEnd(span) + ) + if (isHashtag(text)) null else LinkSpanInfo(text.toString(), span.url) + } + .filterNotNull() + } else { + emptySequence() + } + } + + private fun getHashtags(status: StatusViewData.Concrete): Sequence<CharSequence> { + val content = status.content + return content.getSpans(0, content.length, Object::class.java) + .asSequence() + .map { span -> + content.subSequence(content.getSpanStart(span), content.getSpanEnd(span)) + } + .filter(this::isHashtag) + } + + private fun forceFocus(host: View) { + interrupt() + host.post { + host.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) + } + } + + private fun interrupt() { + a11yManager.interrupt() + } + + private fun isHashtag(text: CharSequence) = text.startsWith("#") + + private val collapseCwAction = AccessibilityActionCompat( + R.id.action_collapse_cw, + context.getString(R.string.post_content_warning_show_less) + ) + + private val expandCwAction = AccessibilityActionCompat( + R.id.action_expand_cw, + context.getString(R.string.post_content_warning_show_more) + ) + + private val replyAction = AccessibilityActionCompat( + R.id.action_reply, + context.getString(R.string.action_reply) + ) + + private val unreblogAction = AccessibilityActionCompat( + R.id.action_unreblog, + context.getString(R.string.action_unreblog) + ) + + private val reblogAction = AccessibilityActionCompat( + R.id.action_reblog, + context.getString(R.string.action_reblog) + ) + + private val unfavouriteAction = AccessibilityActionCompat( + R.id.action_unfavourite, + context.getString(R.string.action_unfavourite) + ) + + private val favouriteAction = AccessibilityActionCompat( + R.id.action_favourite, + context.getString(R.string.action_favourite) + ) + + private val bookmarkAction = AccessibilityActionCompat( + R.id.action_bookmark, + context.getString(R.string.action_bookmark) + ) + + private val unbookmarkAction = AccessibilityActionCompat( + R.id.action_unbookmark, + context.getString(R.string.action_bookmark) + ) + + private val openProfileAction = AccessibilityActionCompat( + R.id.action_open_profile, + context.getString(R.string.action_view_profile) + ) + + private val linksAction = AccessibilityActionCompat( + R.id.action_links, + context.getString(R.string.action_links) + ) + + private val mentionsAction = AccessibilityActionCompat( + R.id.action_mentions, + context.getString(R.string.action_mentions) + ) + + private val hashtagsAction = AccessibilityActionCompat( + R.id.action_hashtags, + context.getString(R.string.action_hashtags) + ) + + private val openRebloggerAction = AccessibilityActionCompat( + R.id.action_open_reblogger, + context.getString(R.string.action_open_reblogger) + ) + + private val openRebloggedByAction = AccessibilityActionCompat( + R.id.action_open_reblogged_by, + context.getString(R.string.action_open_reblogged_by) + ) + + private val openFavsAction = AccessibilityActionCompat( + R.id.action_open_faved_by, + context.getString(R.string.action_open_faved_by) + ) + + private val moreAction = AccessibilityActionCompat( + R.id.action_more, + context.getString(R.string.action_more) + ) + + private data class LinkSpanInfo(val text: String, val link: String) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt new file mode 100644 index 0000000..12e5b73 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ListUtils.kt @@ -0,0 +1,45 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +@file:JvmName("ListUtils") + +package com.keylesspalace.tusky.util + +/** + * Copies elements to destination, removing duplicates and preserving original order. + */ +fun <T, C : MutableCollection<in T>> Iterable<T>.removeDuplicatesTo(destination: C): C { + return filterTo(destination, HashSet<T>()::add) +} + +inline fun <T> List<T>.withoutFirstWhich(predicate: (T) -> Boolean): List<T> { + val index = indexOfFirst(predicate) + if (index == -1) { + return this + } + val newList = toMutableList() + newList.removeAt(index) + return newList +} + +inline fun <T> List<T>.replacedFirstWhich(replacement: T, predicate: (T) -> Boolean): List<T> { + val index = indexOfFirst(predicate) + if (index == -1) { + return this + } + val newList = toMutableList() + newList[index] = replacement + return newList +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LocaleExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/LocaleExtensions.kt new file mode 100644 index 0000000..800e7a4 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/LocaleExtensions.kt @@ -0,0 +1,36 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import android.content.Context +import com.keylesspalace.tusky.R +import java.util.Locale + +// When a language code has changed, `language` *explicitly* returns the obsolete version, +// but `toLanguageTag()` uses the current version +// https://developer.android.com/reference/java/util/Locale#getLanguage() +val Locale.modernLanguageCode: String + get() { + return this.toLanguageTag().split('-', limit = 2)[0] + } + +fun Locale.getTuskyDisplayName(context: Context): String { + return context.getString( + R.string.language_display_name_format, + displayLanguage, + getDisplayLanguage(this) + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt b/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt new file mode 100644 index 0000000..8f3d2a7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/LocaleManager.kt @@ -0,0 +1,105 @@ +/* Copyright 2019 Mélanie Chauvel (ariasuni) + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import android.content.Context +import android.content.SharedPreferences +import android.os.Build +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.os.LocaleListCompat +import androidx.preference.PreferenceDataStore +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.settings.PrefKeys +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class LocaleManager @Inject constructor( + @ApplicationContext val context: Context +) : PreferenceDataStore() { + + @Inject + lateinit var preferences: SharedPreferences + + fun setLocale() { + val language = preferences.getNonNullString(PrefKeys.LANGUAGE, DEFAULT) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + if (language != HANDLED_BY_SYSTEM) { + // app is being opened on Android 13+ for the first time + // hand over the old setting to the system and save a dummy value in Shared Preferences + applyLanguageToApp(language) + + preferences.edit() + .putString(PrefKeys.LANGUAGE, HANDLED_BY_SYSTEM) + .apply() + } + } else { + // on Android < 13 we have to apply the language at every app start + applyLanguageToApp(language) + } + } + + override fun putString(key: String?, value: String?) { + // if we are on Android < 13 we have to save the selected language so we can apply it at appstart + // on Android 13+ the system handles it for us + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + preferences.edit() + .putString(PrefKeys.LANGUAGE, value) + .apply() + } + applyLanguageToApp(value) + } + + override fun getString(key: String?, defValue: String?): String? { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + val selectedLanguage = AppCompatDelegate.getApplicationLocales() + + if (selectedLanguage.isEmpty) { + DEFAULT + } else { + // Android lets users select all variants of languages we support in the system settings, + // so we need to find the closest match + // it should not happen that we find no match, but returning null is fine (picker will show default) + + val availableLanguages = context.resources.getStringArray(R.array.language_values) + + return availableLanguages.find { it == selectedLanguage[0]!!.toLanguageTag() } + ?: availableLanguages.find { language -> + language.startsWith(selectedLanguage[0]!!.language) + } + } + } else { + preferences.getNonNullString(PrefKeys.LANGUAGE, DEFAULT) + } + } + + private fun applyLanguageToApp(language: String?) { + val localeList = if (language == DEFAULT) { + LocaleListCompat.getEmptyLocaleList() + } else { + LocaleListCompat.forLanguageTags(language) + } + + AppCompatDelegate.setApplicationLocales(localeList) + } + + companion object { + private const val DEFAULT = "default" + private const val HANDLED_BY_SYSTEM = "handled_by_system" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/LocaleUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/LocaleUtils.kt new file mode 100644 index 0000000..0103218 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/LocaleUtils.kt @@ -0,0 +1,87 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import android.util.Log +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.os.LocaleListCompat +import com.keylesspalace.tusky.db.entity.AccountEntity +import java.util.Locale + +private const val TAG: String = "LocaleUtils" + +private fun LocaleListCompat.toList(): List<Locale> { + val list = mutableListOf<Locale>() + for (index in 0 until this.size()) { + this[index]?.let { list.add(it) } + } + return list +} + +// Ensure that the locale whose code matches the given language is first in the list +private fun ensureLanguagesAreFirst(locales: MutableList<Locale>, languages: List<String>) { + for (language in languages.reversed()) { + // Iterate prioritized languages in reverse to retain the order once bubbled to the top + var currentLocaleIndex = locales.indexOfFirst { it.language == language } + if (currentLocaleIndex < 0) { + // Recheck against modern language codes + // This should only happen when replying or when the per-account post language is set + // to a modern code + currentLocaleIndex = locales.indexOfFirst { it.modernLanguageCode == language } + + if (currentLocaleIndex < 0) { + // This can happen when: + // - Your per-account posting language is set to one android doesn't know (e.g. toki pona) + // - Replying to a post in a language android doesn't know + locales.add(0, Locale(language)) + Log.w(TAG, "Attempting to use unknown language tag '$language'") + continue + } + } + + if (currentLocaleIndex > 0) { + // Move preselected locale to the top + locales.add(0, locales.removeAt(currentLocaleIndex)) + } + } +} + +fun getInitialLanguages( + language: String? = null, + activeAccount: AccountEntity? = null +): List<String> { + val selected = listOfNotNull(language, activeAccount?.defaultPostLanguage) + val system = AppCompatDelegate.getApplicationLocales().toList() + + LocaleListCompat.getDefault().toList() + + return (selected + system.map { it.language }).distinct().filter { it.isNotEmpty() } +} + +fun getLocaleList(initialLanguages: List<String>): List<Locale> { + val locales = Locale.getAvailableLocales().filter { + // Only "base" languages, "en" but not "en_DK" + it.country.isNullOrEmpty() && + it.script.isNullOrEmpty() && + it.variant.isNullOrEmpty() + }.sortedBy { it.displayName }.toMutableList() + ensureLanguagesAreFirst(locales, initialLanguages) + return locales +} + +fun localeNameForUntrustedISO639LangCode(code: String): String { + // It seems like it never throws? + return Locale(code).displayName +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt new file mode 100644 index 0000000..27c4f54 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/MediaUtils.kt @@ -0,0 +1,198 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import android.content.ContentResolver +import android.database.Cursor +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Matrix +import android.net.Uri +import android.provider.OpenableColumns +import android.util.Log +import androidx.exifinterface.media.ExifInterface +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import kotlin.time.Duration.Companion.hours + +/** + * Helper methods for obtaining and resizing media files + */ +private const val TAG = "MediaUtils" +private const val MEDIA_TEMP_PREFIX = "Tusky_Share_Media" +const val MEDIA_SIZE_UNKNOWN = -1L + +/** + * Fetches the size of the media represented by the given URI, assuming it is openable and + * the ContentResolver is able to resolve it. + * + * @return the size of the media in bytes or {@link MediaUtils#MEDIA_SIZE_UNKNOWN} + */ +fun getMediaSize(contentResolver: ContentResolver, uri: Uri?): Long { + if (uri == null) { + return MEDIA_SIZE_UNKNOWN + } + + var mediaSize = MEDIA_SIZE_UNKNOWN + val cursor: Cursor? + try { + cursor = contentResolver.query(uri, null, null, null, null) + } catch (e: SecurityException) { + return MEDIA_SIZE_UNKNOWN + } + if (cursor != null) { + val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) + cursor.moveToFirst() + mediaSize = cursor.getLong(sizeIndex) + cursor.close() + } + return mediaSize +} + +@Throws(FileNotFoundException::class) +fun getImageSquarePixels(contentResolver: ContentResolver, uri: Uri): Int { + val input = contentResolver.openInputStream(uri) ?: throw FileNotFoundException("Unavailable ContentProvider") + + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + try { + BitmapFactory.decodeStream(input, null, options) + } finally { + input.closeQuietly() + } + + return options.outWidth * options.outHeight +} + +fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { + // Raw height and width of image + val height = options.outHeight + val width = options.outWidth + var inSampleSize = 1 + + if (height > reqHeight || width > reqWidth) { + val halfHeight = height / 2 + val halfWidth = width / 2 + + // Calculate the largest inSampleSize value that is a power of 2 and keeps both + // height and width larger than the requested height and width. + while ((halfHeight / inSampleSize) > reqHeight && (halfWidth / inSampleSize) > reqWidth) { + inSampleSize *= 2 + } + } + + return inSampleSize +} + +fun reorientBitmap(bitmap: Bitmap?, orientation: Int): Bitmap? { + val matrix = Matrix() + when (orientation) { + ExifInterface.ORIENTATION_NORMAL -> return bitmap + ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1.0f, 1.0f) + ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180.0f) + ExifInterface.ORIENTATION_FLIP_VERTICAL -> { + matrix.setRotate(180.0f) + matrix.postScale(-1.0f, 1.0f) + } + ExifInterface.ORIENTATION_TRANSPOSE -> { + matrix.setRotate(90.0f) + matrix.postScale(-1.0f, 1.0f) + } + ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90.0f) + ExifInterface.ORIENTATION_TRANSVERSE -> { + matrix.setRotate(-90.0f) + matrix.postScale(-1.0f, 1.0f) + } + ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90.0f) + else -> return bitmap + } + + if (bitmap == null) { + return null + } + + return try { + val result = Bitmap.createBitmap( + bitmap, + 0, + 0, + bitmap.width, + bitmap.height, + matrix, + true + ) + if (!bitmap.sameAs(result)) { + bitmap.recycle() + } + result + } catch (e: OutOfMemoryError) { + null + } +} + +fun getImageOrientation(uri: Uri, contentResolver: ContentResolver): Int { + try { + val inputStream = contentResolver.openInputStream(uri) + ?: return ExifInterface.ORIENTATION_UNDEFINED + + try { + val exifInterface = ExifInterface(inputStream) + return exifInterface.getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_NORMAL + ) + } finally { + inputStream.closeQuietly() + } + } catch (e: IOException) { + Log.w(TAG, e) + return ExifInterface.ORIENTATION_UNDEFINED + } +} + +fun deleteStaleCachedMedia(mediaDirectory: File?) { + if (mediaDirectory == null || !mediaDirectory.exists()) { + // Nothing to do + return + } + + val unixTime = System.currentTimeMillis() - 24.hours.inWholeMilliseconds + + val files = mediaDirectory.listFiles { file -> unixTime > file.lastModified() && file.name.contains(MEDIA_TEMP_PREFIX) } + if (files.isNullOrEmpty()) { + // Nothing to do + return + } + + for (file in files) { + try { + file.delete() + } catch (se: SecurityException) { + Log.e(TAG, "Error removing stale cached media") + } + } +} + +fun getTemporaryMediaFilename(extension: String): String { + return "${MEDIA_TEMP_PREFIX}_${SimpleDateFormat( + "yyyyMMdd_HHmmss", + Locale.US + ).format(Date())}.$extension" +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/NoUnderlineURLSpan.kt b/app/src/main/java/com/keylesspalace/tusky/util/NoUnderlineURLSpan.kt new file mode 100644 index 0000000..59d8bba --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/NoUnderlineURLSpan.kt @@ -0,0 +1,44 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import android.text.TextPaint +import android.text.style.URLSpan +import android.view.View + +open class NoUnderlineURLSpan(val url: String) : URLSpan(url) { + + // This should not be necessary. But if you don't do this the [StatusLengthTest] tests + // fail. Without this, accessing the `url` property, or calling `getUrl()` (which should + // automatically call through to [UrlSpan.getURL]) returns null. + override fun getURL(): String { + return url + } + + override fun updateDrawState(ds: TextPaint) { + super.updateDrawState(ds) + ds.isUnderlineText = false + } + + override fun onClick(view: View) { + view.context.openLink(url) + } +} + +/** + * Mentions of other users ("@user@example.org") + */ +open class MentionSpan(url: String) : NoUnderlineURLSpan(url) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/NotificationTypeConverter.kt b/app/src/main/java/com/keylesspalace/tusky/util/NotificationTypeConverter.kt new file mode 100644 index 0000000..969deba --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/NotificationTypeConverter.kt @@ -0,0 +1,46 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import com.keylesspalace.tusky.entity.Notification +import org.json.JSONArray + +/** + * Serialize to string array and deserialize notifications type + */ + +fun serialize(data: Set<Notification.Type>?): String { + val array = JSONArray() + data?.forEach { + array.put(it.presentation) + } + return array.toString() +} + +fun deserialize(data: String?): Set<Notification.Type> { + val ret = HashSet<Notification.Type>() + data?.let { + val array = JSONArray(data) + for (i in 0 until array.length()) { + val item = array.getString(i) + val type = Notification.Type.byString(item) + if (type != Notification.Type.UNKNOWN) { + ret.add(type) + } + } + } + return ret +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/NumberUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/NumberUtils.kt new file mode 100644 index 0000000..29a2ec6 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/NumberUtils.kt @@ -0,0 +1,28 @@ +@file:JvmName("NumberUtils") + +package com.keylesspalace.tusky.util + +import java.text.NumberFormat +import kotlin.math.abs +import kotlin.math.ln +import kotlin.math.pow + +private val numberFormatter: NumberFormat = NumberFormat.getInstance() +private val ln_1k = ln(1000.0) + +/** + * Format numbers according to the current locale. Numbers < min have + * separators (',', '.', etc) inserted according to the locale. + * + * Numbers >= min are scaled down to that by multiples of 1,000, and + * a suffix appropriate to the scaling is appended. + */ +fun formatNumber(num: Long, min: Int = 100000): String { + val absNum = abs(num) + if (absNum < min) return numberFormatter.format(num) + + val exp = (ln(absNum.toDouble()) / ln_1k).toInt() + + // Suffixes here are locale-agnostic + return String.format("%.1f%c", num / 1000.0.pow(exp.toDouble()), "KMGTPE"[exp - 1]) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/PickMediaFiles.kt b/app/src/main/java/com/keylesspalace/tusky/util/PickMediaFiles.kt new file mode 100644 index 0000000..a5f4655 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/PickMediaFiles.kt @@ -0,0 +1,52 @@ +/* Copyright 2021 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.net.Uri +import androidx.activity.result.contract.ActivityResultContract + +class PickMediaFiles : ActivityResultContract<Boolean, List<Uri>>() { + override fun createIntent(context: Context, input: Boolean): Intent { + return Intent(Intent.ACTION_GET_CONTENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType("*/*") + .apply { + putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*", "video/*", "audio/*")) + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, input) + } + } + + override fun parseResult(resultCode: Int, intent: Intent?): List<Uri> { + if (resultCode == Activity.RESULT_OK) { + val intentData = intent?.data + val clipData = intent?.clipData + if (intentData != null) { + // Single media, upload it and done. + return listOf(intentData) + } else if (clipData != null) { + val result: MutableList<Uri> = mutableListOf() + for (i in 0 until clipData.itemCount) { + result.add(clipData.getItemAt(i).uri) + } + return result + } + } + return emptyList() + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/RelativeTimeUpdater.kt b/app/src/main/java/com/keylesspalace/tusky/util/RelativeTimeUpdater.kt new file mode 100644 index 0000000..9af2323 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/RelativeTimeUpdater.kt @@ -0,0 +1,36 @@ +@file:JvmName("RelativeTimeUpdater") + +package com.keylesspalace.tusky.util + +import android.content.SharedPreferences +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.coroutineScope +import androidx.lifecycle.repeatOnLifecycle +import com.keylesspalace.tusky.settings.PrefKeys +import kotlin.time.Duration.Companion.minutes +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +private val UPDATE_INTERVAL = 1.minutes + +/** + * Helper method to update adapter periodically to refresh timestamp + * if setting absoluteTimeView is false. + * Start updates when the Fragment becomes visible and stop when it is hidden. + */ +fun Fragment.updateRelativeTimePeriodically(preferences: SharedPreferences, callback: Runnable) { + val lifecycle = viewLifecycleOwner.lifecycle + lifecycle.coroutineScope.launch { + // This child coroutine will launch each time the Fragment moves to the STARTED state + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + val useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false) + if (!useAbsoluteTime) { + while (true) { + callback.run() + delay(UPDATE_INTERVAL) + } + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/Resource.kt b/app/src/main/java/com/keylesspalace/tusky/util/Resource.kt new file mode 100644 index 0000000..0f212b0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/Resource.kt @@ -0,0 +1,16 @@ +package com.keylesspalace.tusky.util + +sealed interface Resource<T> { + val data: T? +} + +class Loading<T>(override val data: T? = null) : Resource<T> + +class Success<T>(override val data: T? = null) : Resource<T> + +class Error<T>( + override val data: T? = null, + val errorMessage: String? = null, + var consumed: Boolean = false, + val cause: Throwable? = null +) : Resource<T> diff --git a/app/src/main/java/com/keylesspalace/tusky/util/RickRoll.kt b/app/src/main/java/com/keylesspalace/tusky/util/RickRoll.kt new file mode 100644 index 0000000..788786e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/RickRoll.kt @@ -0,0 +1,20 @@ +package com.keylesspalace.tusky.util + +import android.content.Context +import android.content.Intent +import android.net.Uri +import com.keylesspalace.tusky.R + +fun shouldRickRoll(context: Context, domain: String) = + context.resources.getStringArray(R.array.rick_roll_domains).any { candidate -> + domain.equals(candidate, true) || domain.endsWith(".$candidate", true) + } + +fun rickRoll(context: Context) { + val uri = Uri.parse(context.getString(R.string.rick_roll_url)) + val intent = Intent(Intent.ACTION_VIEW, uri).apply { + addCategory(Intent.CATEGORY_BROWSABLE) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + context.startActivity(intent) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt new file mode 100644 index 0000000..d92df6f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ShareShortcutHelper.kt @@ -0,0 +1,112 @@ +/* Copyright 2019 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +@file:JvmName("ShareShortcutHelper") + +package com.keylesspalace.tusky.util + +import android.content.Context +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.Canvas +import android.util.Log +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.app.Person +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat +import com.bumptech.glide.Glide +import com.bumptech.glide.load.engine.GlideException +import com.keylesspalace.tusky.MainActivity +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.di.ApplicationScope +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +class ShareShortcutHelper @Inject constructor( + @ApplicationContext private val context: Context, + private val accountManager: AccountManager, + @ApplicationScope private val externalScope: CoroutineScope +) { + + fun updateShortcuts() { + externalScope.launch(Dispatchers.IO) { + val innerSize = context.resources.getDimensionPixelSize(R.dimen.adaptive_bitmap_inner_size) + val outerSize = context.resources.getDimensionPixelSize(R.dimen.adaptive_bitmap_outer_size) + + val maxNumberOfShortcuts = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context) + + val shortcuts = accountManager.accounts.take(maxNumberOfShortcuts).mapNotNull { account -> + + val drawable = try { + Glide.with(context) + .asDrawable() + .load(account.profilePictureUrl) + .submitAsync(innerSize, innerSize) + } catch (e: GlideException) { + // https://github.com/bumptech/glide/issues/4672 :/ + Log.w(TAG, "failed to load avatar ${account.profilePictureUrl}", e) + AppCompatResources.getDrawable(context, R.drawable.avatar_default) ?: return@mapNotNull null + } + + // inset the loaded bitmap inside a 108dp transparent canvas so it looks good as adaptive icon + val outBmp = Bitmap.createBitmap(outerSize, outerSize, Bitmap.Config.ARGB_8888) + + val canvas = Canvas(outBmp) + val borderSize = (outerSize - innerSize) / 2 + drawable.setBounds(borderSize, borderSize, borderSize + innerSize, borderSize + innerSize) + drawable.draw(canvas) + + val icon = IconCompat.createWithAdaptiveBitmap(outBmp) + + val person = Person.Builder() + .setIcon(icon) + .setName(account.displayName) + .setKey(account.identifier) + .build() + + // This intent will be sent when the user clicks on one of the launcher shortcuts. Intent from share sheet will be different + val intent = Intent(context, MainActivity::class.java).apply { + action = Intent.ACTION_SEND + type = "text/plain" + putExtra(ShortcutManagerCompat.EXTRA_SHORTCUT_ID, account.id.toString()) + } + + ShortcutInfoCompat.Builder(context, account.id.toString()) + .setIntent(intent) + .setCategories(setOf("com.keylesspalace.tusky.Share")) + .setShortLabel(account.displayName) + .setPerson(person) + .setIcon(icon) + .build() + } + + ShortcutManagerCompat.setDynamicShortcuts(context, shortcuts) + } + } + + fun removeShortcut(account: AccountEntity) { + ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(account.id.toString())) + } + + companion object { + private const val TAG = "ShareShortcutHelper" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SharedPreferencesExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/SharedPreferencesExtensions.kt new file mode 100644 index 0000000..a3835a8 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/SharedPreferencesExtensions.kt @@ -0,0 +1,7 @@ +package com.keylesspalace.tusky.util + +import android.content.SharedPreferences + +fun SharedPreferences.getNonNullString(key: String, defValue: String): String { + return this.getString(key, defValue) ?: defValue +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.kt b/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.kt new file mode 100644 index 0000000..c5e7e1a --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/SmartLengthInputFilter.kt @@ -0,0 +1,108 @@ +/* Copyright 2019 Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import android.icu.text.BreakIterator +import android.text.InputFilter +import android.text.SpannableStringBuilder +import android.text.Spanned + +/** + * Defines how many characters to extend beyond the limit to cut at the end of the word on the + * boundary of it rather than cutting at the word preceding that one. + */ +private const val RUNWAY = 10 + +/** + * Default for maximum status length on Mastodon and default collapsing length on Pleroma. + */ +private const val LENGTH_DEFAULT = 500 + +/** + * Calculates if it's worth trimming the message at a specific limit or if the content that will + * be hidden will not be enough to justify the operation. + * + * @param message The message to trim. + * @return Whether the message should be trimmed or not. + */ +fun shouldTrimStatus(message: Spanned): Boolean { + return message.isNotEmpty() && LENGTH_DEFAULT.toFloat() / message.length < 0.75 +} + +/** + * A customized version of {@link android.text.InputFilter.LengthFilter} which allows smarter + * constraints and adds better visuals such as: + * <ul> + * <li>Ellipsis at the end of the constrained text to show continuation.</li> + * <li>Trimming of invisible characters (new lines, spaces, etc.) from the constrained text.</li> + * <li>Constraints end at the end of the last "word", before a whitespace.</li> + * <li>Expansion of the limit by up to 10 characters to facilitate the previous constraint.</li> + * <li>Constraints are not applied if the percentage of hidden content is too small.</li> + * </ul> + */ +object SmartLengthInputFilter : InputFilter { + /** {@inheritDoc} */ + override fun filter( + source: CharSequence, + start: Int, + end: Int, + dest: Spanned, + dstart: Int, + dend: Int + ): CharSequence? { + // Code originally imported from InputFilter.LengthFilter but heavily customized and converted to Kotlin. + // https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/text/InputFilter.java#175 + + val sourceLength = source.length + var keep = LENGTH_DEFAULT - (dest.length - (dend - dstart)) + if (keep <= 0) return "" + if (keep >= end - start) return null // Keep original + + keep += start + + // Skip trimming if the ratio doesn't warrant it + if (keep.toDouble() / sourceLength > 0.75) return null + + // Enable trimming at the end of the closest word if possible + if (source[keep].isLetterOrDigit()) { + var boundary: Int + + val iterator = BreakIterator.getWordInstance() + iterator.setText(source.toString()) + boundary = iterator.following(keep) + if (keep - boundary > RUNWAY) boundary = iterator.preceding(keep) + + keep = boundary + } else { + // If no runway is allowed simply remove whitespace if present + while (source[keep - 1].isWhitespace()) { + --keep + if (keep == start) return "" + } + } + + if (source[keep - 1].isHighSurrogate()) { + --keep + if (keep == start) return "" + } + + return if (source is Spanned) { + SpannableStringBuilder(source, start, keep).append("…") + } else { + "${source.subSequence(start, keep)}…" + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt new file mode 100644 index 0000000..0a6b1ca --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/SpanUtils.kt @@ -0,0 +1,132 @@ +package com.keylesspalace.tusky.util + +import android.content.Context +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.CharacterStyle +import android.text.style.DynamicDrawableSpan +import android.text.style.ForegroundColorSpan +import android.text.style.ImageSpan +import android.text.style.URLSpan +import com.keylesspalace.tusky.util.twittertext.Regex +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial +import java.util.regex.Pattern + +/** + * @see <a href="https://github.com/tootsuite/mastodon/blob/master/app/models/tag.rb"> + * Tag#HASHTAG_RE</a>. + */ +private const val HASHTAG_SEPARATORS = "_\\u00B7\\u30FB\\u200c" +internal const val TAG_PATTERN_STRING = "(?<![=/)\\p{Alnum}])(#(([\\w_][\\w$HASHTAG_SEPARATORS]*[\\p{Alpha}$HASHTAG_SEPARATORS][\\w$HASHTAG_SEPARATORS]*[\\w_])|([\\w_]*[\\p{Alpha}][\\w_]*)))" +private val TAG_PATTERN = TAG_PATTERN_STRING.toPattern(Pattern.CASE_INSENSITIVE) + +/** + * @see <a href="https://github.com/tootsuite/mastodon/blob/master/app/models/account.rb"> + * Account#MENTION_RE</a> + */ +private const val USERNAME_PATTERN_STRING = "[a-z0-9_]+([a-z0-9_.-]+[a-z0-9_]+)?" +internal const val MENTION_PATTERN_STRING = "(?<![=/\\w])(@($USERNAME_PATTERN_STRING)(?:@[\\w.-]+[\\w]+)?)" +private val MENTION_PATTERN = MENTION_PATTERN_STRING.toPattern(Pattern.CASE_INSENSITIVE) + +private val VALID_URL_PATTERN = Regex.VALID_URL_PATTERN_STRING.toPattern(Pattern.CASE_INSENSITIVE) + +private val spanClasses = listOf(ForegroundColorSpan::class.java, URLSpan::class.java) + +// url must come first, it may contain the other patterns +val defaultFinders = listOf( + PatternFinder("http", FoundMatchType.HTTP_URL, VALID_URL_PATTERN), + PatternFinder("#", FoundMatchType.TAG, TAG_PATTERN), + PatternFinder("@", FoundMatchType.MENTION, MENTION_PATTERN) +) + +enum class FoundMatchType { + HTTP_URL, + HTTPS_URL, + TAG, + MENTION +} + +class PatternFinder( + val searchString: String, + val type: FoundMatchType, + val pattern: Pattern +) + +/** + * Takes text containing mentions and hashtags and urls and makes them the given colour. + * @param finders The finders to use. This is here so they can be overridden from unit tests. + */ +fun Spannable.highlightSpans(colour: Int, finders: List<PatternFinder> = defaultFinders) { + // Strip all existing colour spans. + for (spanClass in spanClasses) { + clearSpans(spanClass) + } + + for (finder in finders) { + // before running the regular expression, check if there is even a chance of it finding something + if (this.contains(finder.searchString)) { + val matcher = finder.pattern.matcher(this) + + while (matcher.find()) { + // we found a match + val start = matcher.start(1) + + val end = matcher.end(1) + + // only add a span if there is no other one yet (e.g. the #anchor part of an url might match as hashtag, but must be ignored) + if (this.getSpans(start, end, URLSpan::class.java).isEmpty()) { + this.setSpan( + getSpan(finder.type, this, colour, start, end), + start, + end, + Spanned.SPAN_INCLUSIVE_EXCLUSIVE + ) + } + } + } + } +} + +private fun <T> Spannable.clearSpans(spanClass: Class<T>) { + for (span in getSpans(0, length, spanClass)) { + removeSpan(span) + } +} + +/** + * Replaces text of the form [iconics name] with their spanned counterparts (ImageSpan). + */ +fun addDrawables(text: CharSequence, color: Int, size: Int, context: Context): Spannable { + val builder = SpannableStringBuilder(text) + + val pattern = Pattern.compile("\\[iconics ([0-9a-z_]+)]") + val matcher = pattern.matcher(builder) + while (matcher.find()) { + val resourceName = matcher.group(1) + ?: continue + + val drawable = IconicsDrawable(context, GoogleMaterial.getIcon(resourceName)) + drawable.setBounds(0, 0, size, size) + drawable.setTint(color) + + builder.setSpan(ImageSpan(drawable, DynamicDrawableSpan.ALIGN_BASELINE), matcher.start(), matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + + return builder +} + +private fun getSpan( + matchType: FoundMatchType, + string: CharSequence, + colour: Int, + start: Int, + end: Int +): CharacterStyle { + return when (matchType) { + FoundMatchType.HTTP_URL, FoundMatchType.HTTPS_URL -> NoUnderlineURLSpan(string.substring(start, end)) + FoundMatchType.MENTION -> MentionSpan(string.substring(start, end)) + else -> ForegroundColorSpan(colour) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt new file mode 100644 index 0000000..3980ac7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusDisplayOptions.kt @@ -0,0 +1,127 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.util + +import android.content.SharedPreferences +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.settings.PrefKeys + +data class StatusDisplayOptions( + @get:JvmName("animateAvatars") + val animateAvatars: Boolean, + @get:JvmName("mediaPreviewEnabled") + val mediaPreviewEnabled: Boolean, + @get:JvmName("useAbsoluteTime") + val useAbsoluteTime: Boolean, + @get:JvmName("showBotOverlay") + val showBotOverlay: Boolean, + @get:JvmName("useBlurhash") + val useBlurhash: Boolean, + @get:JvmName("cardViewMode") + val cardViewMode: CardViewMode, + @get:JvmName("confirmReblogs") + val confirmReblogs: Boolean, + @get:JvmName("confirmFavourites") + val confirmFavourites: Boolean, + @get:JvmName("hideStats") + val hideStats: Boolean, + @get:JvmName("animateEmojis") + val animateEmojis: Boolean, + @get:JvmName("showStatsInline") + val showStatsInline: Boolean, + @get:JvmName("showSensitiveMedia") + val showSensitiveMedia: Boolean, + @get:JvmName("openSpoiler") + val openSpoiler: Boolean +) { + + /** + * @return a new StatusDisplayOptions adapted to whichever preference changed. + */ + fun make(preferences: SharedPreferences, key: String, account: AccountEntity) = when (key) { + PrefKeys.ANIMATE_GIF_AVATARS -> copy( + animateAvatars = preferences.getBoolean(key, false) + ) + PrefKeys.MEDIA_PREVIEW_ENABLED -> copy( + mediaPreviewEnabled = account.mediaPreviewEnabled + ) + PrefKeys.ABSOLUTE_TIME_VIEW -> copy( + useAbsoluteTime = preferences.getBoolean(key, false) + ) + PrefKeys.SHOW_BOT_OVERLAY -> copy( + showBotOverlay = preferences.getBoolean(key, true) + ) + PrefKeys.USE_BLURHASH -> copy( + useBlurhash = preferences.getBoolean(key, true) + ) + PrefKeys.CONFIRM_FAVOURITES -> copy( + confirmFavourites = preferences.getBoolean(key, false) + ) + PrefKeys.CONFIRM_REBLOGS -> copy( + confirmReblogs = preferences.getBoolean(key, true) + ) + PrefKeys.WELLBEING_HIDE_STATS_POSTS -> copy( + hideStats = preferences.getBoolean(key, false) + ) + PrefKeys.ANIMATE_CUSTOM_EMOJIS -> copy( + animateEmojis = preferences.getBoolean(key, false) + ) + PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA -> copy( + showSensitiveMedia = account.alwaysShowSensitiveMedia + ) + PrefKeys.ALWAYS_OPEN_SPOILER -> copy( + openSpoiler = account.alwaysOpenSpoiler + ) + else -> { + this + } + } + + companion object { + /** Preference keys that, if changed, affect StatusDisplayOptions */ + val prefKeys = setOf( + PrefKeys.ABSOLUTE_TIME_VIEW, + PrefKeys.ALWAYS_SHOW_SENSITIVE_MEDIA, + PrefKeys.ALWAYS_OPEN_SPOILER, + PrefKeys.ANIMATE_CUSTOM_EMOJIS, + PrefKeys.ANIMATE_GIF_AVATARS, + PrefKeys.CONFIRM_FAVOURITES, + PrefKeys.CONFIRM_REBLOGS, + PrefKeys.MEDIA_PREVIEW_ENABLED, + PrefKeys.SHOW_BOT_OVERLAY, + PrefKeys.USE_BLURHASH, + PrefKeys.WELLBEING_HIDE_STATS_POSTS + ) + + fun from(preferences: SharedPreferences, account: AccountEntity) = StatusDisplayOptions( + animateAvatars = preferences.getBoolean(PrefKeys.ANIMATE_GIF_AVATARS, false), + animateEmojis = preferences.getBoolean(PrefKeys.ANIMATE_CUSTOM_EMOJIS, false), + mediaPreviewEnabled = account.mediaPreviewEnabled, + useAbsoluteTime = preferences.getBoolean(PrefKeys.ABSOLUTE_TIME_VIEW, false), + showBotOverlay = preferences.getBoolean(PrefKeys.SHOW_BOT_OVERLAY, true), + useBlurhash = preferences.getBoolean(PrefKeys.USE_BLURHASH, true), + cardViewMode = CardViewMode.NONE, + confirmReblogs = preferences.getBoolean(PrefKeys.CONFIRM_REBLOGS, true), + confirmFavourites = preferences.getBoolean(PrefKeys.CONFIRM_FAVOURITES, false), + hideStats = preferences.getBoolean(PrefKeys.WELLBEING_HIDE_STATS_POSTS, false), + showStatsInline = preferences.getBoolean(PrefKeys.SHOW_STATS_INLINE, false), + showSensitiveMedia = account.alwaysShowSensitiveMedia, + openSpoiler = account.alwaysOpenSpoiler + ) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt new file mode 100644 index 0000000..56d60e9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusParsingHelper.kt @@ -0,0 +1,94 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +@file:JvmName("StatusParsingHelper") + +package com.keylesspalace.tusky.util + +import android.text.Editable +import android.text.Html.TagHandler +import android.text.Spannable +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.TypefaceSpan +import androidx.core.text.parseAsHtml +import org.xml.sax.XMLReader + +/** + * parse a String containing html from the Mastodon api to Spanned + */ +@JvmOverloads +fun String.parseAsMastodonHtml(tagHandler: TagHandler? = tuskyTagHandler): Spanned { + return this.replace("<br> ", "<br> ") + .replace("<br /> ", "<br /> ") + .replace("<br/> ", "<br/> ") + .replace("\n", "<br/>") + .replace(" ", " ") + .parseAsHtml(tagHandler = tagHandler) + /* Html.fromHtml returns trailing whitespace if the html ends in a </p> tag, which + * most status contents do, so it should be trimmed. */ + .trimTrailingWhitespace() +} + +val tuskyTagHandler = TuskyTagHandler() + +open class TuskyTagHandler : TagHandler { + + class Code + + override fun handleTag(opening: Boolean, tag: String, output: Editable, xmlReader: XMLReader) { + when (tag) { + "code" -> { + if (opening) { + start(output as SpannableStringBuilder, Code()) + } else { + end( + output as SpannableStringBuilder, + Code::class.java, + TypefaceSpan("monospace") + ) + } + } + } + } + + /** @return the last span in [text] of type [kind], or null if that kind is not in text */ + protected fun <T> getLast(text: Spanned, kind: Class<T>): Any? { + val spans = text.getSpans(0, text.length, kind) + return spans?.get(spans.size - 1) + } + + /** + * Mark the start of a span of [text] with [mark] so it can be discovered later by [end]. + */ + protected fun start(text: SpannableStringBuilder, mark: Any) { + val len = text.length + text.setSpan(mark, len, len, Spannable.SPAN_MARK_MARK) + } + + /** + * Set a [span] over the [text] from the point recently marked with [mark] to the end + * of the text. + */ + protected fun <T> end(text: SpannableStringBuilder, mark: Class<T>, span: Any) { + val len = text.length + val obj = getLast(text, mark) + val where = text.getSpanStart(obj) + text.removeSpan(obj) + if (where != len) { + text.setSpan(span, where, len, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt new file mode 100644 index 0000000..ee575ce --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/StatusViewHelper.kt @@ -0,0 +1,374 @@ +/* Copyright 2019 Joel Pyska + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.ColorDrawable +import android.text.InputFilter +import android.text.TextUtils +import android.view.View +import android.widget.ImageView +import android.widget.TextView +import androidx.annotation.DrawableRes +import com.bumptech.glide.Glide +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Emoji +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.view.MediaPreviewImageView +import com.keylesspalace.tusky.viewdata.PollViewData +import com.keylesspalace.tusky.viewdata.buildDescription +import com.keylesspalace.tusky.viewdata.calculatePercent +import java.text.NumberFormat +import kotlin.math.min + +class StatusViewHelper(private val itemView: View) { + private val absoluteTimeFormatter = AbsoluteTimeFormatter() + + interface MediaPreviewListener { + fun onViewMedia(v: View?, idx: Int) + fun onContentHiddenChange(isShowing: Boolean) + } + + fun setMediasPreview( + statusDisplayOptions: StatusDisplayOptions, + attachments: List<Attachment>, + sensitive: Boolean, + previewListener: MediaPreviewListener, + showingContent: Boolean, + mediaPreviewHeight: Int + ) { + val context = itemView.context + val mediaPreviews = arrayOf<MediaPreviewImageView>( + itemView.findViewById(R.id.status_media_preview_0), + itemView.findViewById(R.id.status_media_preview_1), + itemView.findViewById(R.id.status_media_preview_2), + itemView.findViewById(R.id.status_media_preview_3) + ) + + val mediaOverlays = arrayOf<ImageView>( + itemView.findViewById(R.id.status_media_overlay_0), + itemView.findViewById(R.id.status_media_overlay_1), + itemView.findViewById(R.id.status_media_overlay_2), + itemView.findViewById(R.id.status_media_overlay_3) + ) + + val sensitiveMediaWarning = itemView.findViewById<TextView>( + R.id.status_sensitive_media_warning + ) + val sensitiveMediaShow = itemView.findViewById<View>(R.id.status_sensitive_media_button) + val mediaLabel = itemView.findViewById<TextView>(R.id.status_media_label) + if (statusDisplayOptions.mediaPreviewEnabled) { + // Hide the unused label. + mediaLabel.visibility = View.GONE + } else { + setMediaLabel(mediaLabel, attachments, sensitive, previewListener) + // Hide all unused views. + mediaPreviews[0].visibility = View.GONE + mediaPreviews[1].visibility = View.GONE + mediaPreviews[2].visibility = View.GONE + mediaPreviews[3].visibility = View.GONE + sensitiveMediaWarning.visibility = View.GONE + sensitiveMediaShow.visibility = View.GONE + return + } + + val mediaPreviewUnloaded = + ColorDrawable( + MaterialColors.getColor(context, R.attr.colorBackgroundAccent, Color.BLACK) + ) + + val n = min(attachments.size, Status.MAX_MEDIA_ATTACHMENTS) + + for (i in 0 until n) { + val attachment = attachments[i] + val previewUrl = attachment.previewUrl + val description = attachment.description + + if (TextUtils.isEmpty(description)) { + mediaPreviews[i].contentDescription = context.getString(R.string.action_view_media) + } else { + mediaPreviews[i].contentDescription = description + } + + mediaPreviews[i].visibility = View.VISIBLE + + if (TextUtils.isEmpty(previewUrl)) { + Glide.with(mediaPreviews[i]) + .load(mediaPreviewUnloaded) + .centerInside() + .into(mediaPreviews[i]) + } else { + val placeholder = if (attachment.blurhash != null) { + decodeBlurHash(context, attachment.blurhash) + } else { + mediaPreviewUnloaded + } + val meta = attachment.meta + val focus = meta?.focus + if (showingContent) { + if (focus != null) { // If there is a focal point for this attachment: + mediaPreviews[i].setFocalPoint(focus) + + Glide.with(mediaPreviews[i]) + .load(previewUrl) + .placeholder(placeholder) + .centerInside() + .addListener(mediaPreviews[i]) + .into(mediaPreviews[i]) + } else { + mediaPreviews[i].removeFocalPoint() + + Glide.with(mediaPreviews[i]) + .load(previewUrl) + .placeholder(placeholder) + .centerInside() + .into(mediaPreviews[i]) + } + } else { + mediaPreviews[i].removeFocalPoint() + if (statusDisplayOptions.useBlurhash && attachment.blurhash != null) { + val blurhashBitmap = decodeBlurHash(context, attachment.blurhash) + mediaPreviews[i].setImageDrawable(blurhashBitmap) + } else { + mediaPreviews[i].setImageDrawable(mediaPreviewUnloaded) + } + } + } + + val type = attachment.type + if (showingContent && + (type === Attachment.Type.VIDEO) or (type === Attachment.Type.GIFV) + ) { + mediaOverlays[i].visibility = View.VISIBLE + } else { + mediaOverlays[i].visibility = View.GONE + } + + mediaPreviews[i].setOnClickListener { v -> + previewListener.onViewMedia(v, i) + } + + if (n <= 2) { + mediaPreviews[0].layoutParams.height = mediaPreviewHeight * 2 + mediaPreviews[1].layoutParams.height = mediaPreviewHeight * 2 + } else { + mediaPreviews[0].layoutParams.height = mediaPreviewHeight + mediaPreviews[1].layoutParams.height = mediaPreviewHeight + mediaPreviews[2].layoutParams.height = mediaPreviewHeight + mediaPreviews[3].layoutParams.height = mediaPreviewHeight + } + } + if (attachments.isEmpty()) { + sensitiveMediaWarning.visibility = View.GONE + sensitiveMediaShow.visibility = View.GONE + } else { + sensitiveMediaWarning.text = if (sensitive) { + context.getString(R.string.post_sensitive_media_title) + } else { + context.getString(R.string.post_media_hidden_title) + } + + sensitiveMediaWarning.visibility = if (showingContent) View.GONE else View.VISIBLE + sensitiveMediaShow.visibility = if (showingContent) View.VISIBLE else View.GONE + sensitiveMediaShow.setOnClickListener { v -> + previewListener.onContentHiddenChange(false) + v.visibility = View.GONE + sensitiveMediaWarning.visibility = View.VISIBLE + setMediasPreview( + statusDisplayOptions, + attachments, + sensitive, + previewListener, + false, + mediaPreviewHeight + ) + } + sensitiveMediaWarning.setOnClickListener { v -> + previewListener.onContentHiddenChange(true) + v.visibility = View.GONE + sensitiveMediaShow.visibility = View.VISIBLE + setMediasPreview( + statusDisplayOptions, + attachments, + sensitive, + previewListener, + true, + mediaPreviewHeight + ) + } + } + + // Hide any of the placeholder previews beyond the ones set. + for (i in n until Status.MAX_MEDIA_ATTACHMENTS) { + mediaPreviews[i].visibility = View.GONE + } + } + + private fun setMediaLabel( + mediaLabel: TextView, + attachments: List<Attachment>, + sensitive: Boolean, + listener: MediaPreviewListener + ) { + if (attachments.isEmpty()) { + mediaLabel.visibility = View.GONE + return + } + mediaLabel.visibility = View.VISIBLE + + // Set the label's text. + val context = mediaLabel.context + var labelText = getLabelTypeText(context, attachments[0].type) + if (sensitive) { + val sensitiveText = context.getString(R.string.post_sensitive_media_title) + labelText += String.format(" (%s)", sensitiveText) + } + mediaLabel.text = labelText + + // Set the icon next to the label. + val drawableId = getLabelIcon(attachments[0].type) + mediaLabel.setCompoundDrawablesRelativeWithIntrinsicBounds(drawableId, 0, 0, 0) + + mediaLabel.setOnClickListener { listener.onViewMedia(null, 0) } + } + + private fun getLabelTypeText(context: Context, type: Attachment.Type): String { + return when (type) { + Attachment.Type.IMAGE -> context.getString(R.string.post_media_images) + Attachment.Type.GIFV, Attachment.Type.VIDEO -> context.getString( + R.string.post_media_video + ) + Attachment.Type.AUDIO -> context.getString(R.string.post_media_audio) + else -> context.getString(R.string.post_media_attachments) + } + } + + @DrawableRes + private fun getLabelIcon(type: Attachment.Type): Int { + return when (type) { + Attachment.Type.IMAGE -> R.drawable.ic_photo_24dp + Attachment.Type.GIFV, Attachment.Type.VIDEO -> R.drawable.ic_videocam_24dp + Attachment.Type.AUDIO -> R.drawable.ic_music_box_24dp + else -> R.drawable.ic_attach_file_24dp + } + } + + fun setupPollReadonly( + poll: PollViewData?, + emojis: List<Emoji>, + statusDisplayOptions: StatusDisplayOptions + ) { + val pollResults = listOf<TextView>( + itemView.findViewById(R.id.status_poll_option_result_0), + itemView.findViewById(R.id.status_poll_option_result_1), + itemView.findViewById(R.id.status_poll_option_result_2), + itemView.findViewById(R.id.status_poll_option_result_3) + ) + + val pollDescription = itemView.findViewById<TextView>(R.id.status_poll_description) + + if (poll == null) { + for (pollResult in pollResults) { + pollResult.visibility = View.GONE + } + pollDescription.visibility = View.GONE + } else { + val timestamp = System.currentTimeMillis() + + setupPollResult(poll, emojis, pollResults, statusDisplayOptions.animateEmojis) + + pollDescription.visibility = View.VISIBLE + pollDescription.text = getPollInfoText(timestamp, poll, pollDescription, statusDisplayOptions.useAbsoluteTime) + } + } + + private fun getPollInfoText( + timestamp: Long, + poll: PollViewData, + pollDescription: TextView, + useAbsoluteTime: Boolean + ): CharSequence { + val context = pollDescription.context + + val votesText = if (poll.votersCount == null) { + val votes = NumberFormat.getNumberInstance().format(poll.votesCount.toLong()) + context.resources.getQuantityString(R.plurals.poll_info_votes, poll.votesCount, votes) + } else { + val votes = NumberFormat.getNumberInstance().format(poll.votersCount.toLong()) + context.resources.getQuantityString(R.plurals.poll_info_people, poll.votersCount, votes) + } + val pollDurationInfo = if (poll.expired) { + context.getString(R.string.poll_info_closed) + } else { + if (useAbsoluteTime) { + context.getString( + R.string.poll_info_time_absolute, + absoluteTimeFormatter.format(poll.expiresAt, false) + ) + } else { + formatPollDuration(context, poll.expiresAt!!.time, timestamp) + } + } + + return context.getString(R.string.poll_info_format, votesText, pollDurationInfo) + } + + private fun setupPollResult( + poll: PollViewData, + emojis: List<Emoji>, + pollResults: List<TextView>, + animateEmojis: Boolean + ) { + val options = poll.options + + for (i in 0 until Status.MAX_POLL_OPTIONS) { + if (i < options.size) { + val percent = + calculatePercent(options[i].votesCount, poll.votersCount, poll.votesCount) + + val pollOptionText = + buildDescription( + options[i].title, + percent, + options[i].voted, + pollResults[i].context + ) + pollResults[i].text = pollOptionText.emojify(emojis, pollResults[i], animateEmojis) + pollResults[i].visibility = View.VISIBLE + + val level = percent * 100 + val optionColor = if (options[i].voted) { + R.color.colorBackgroundHighlight + } else { + R.color.colorBackgroundAccent + } + + pollResults[i].background.level = level + pollResults[i].background.setTint(pollResults[i].context.getColor(optionColor)) + } else { + pollResults[i].visibility = View.GONE + } + } + } + + companion object { + val COLLAPSE_INPUT_FILTER = arrayOf<InputFilter>(SmartLengthInputFilter) + val NO_INPUT_FILTER = arrayOfNulls<InputFilter>(0) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt new file mode 100644 index 0000000..3a9388d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/StringUtils.kt @@ -0,0 +1,63 @@ +@file:JvmName("StringUtils") + +package com.keylesspalace.tusky.util + +import android.text.Spanned +import kotlin.random.Random + +private const val POSSIBLE_CHARS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + +fun randomAlphanumericString(count: Int): String { + val chars = CharArray(count) + for (i in 0 until count) { + chars[i] = POSSIBLE_CHARS[Random.nextInt(POSSIBLE_CHARS.length)] + } + return String(chars) +} + +/** + * A < B (strictly) by length and then by content. + * Examples: + * "abc" < "bcd" + * "ab" < "abc" + * "cb" < "abc" + * not: "ab" < "ab" + * not: "abc" > "cb" + */ +fun String.isLessThan(other: String): Boolean { + return when { + this.length < other.length -> true + this.length > other.length -> false + else -> this < other + } +} + +/** + * A <= B (strictly) by length and then by content. + * Examples: + * "abc" <= "bcd" + * "ab" <= "abc" + * "cb" <= "abc" + * "ab" <= "ab" + * not: "abc" > "cb" + */ +fun String.isLessThanOrEqual(other: String): Boolean { + return this == other || isLessThan(other) +} + +fun Spanned.trimTrailingWhitespace(): Spanned { + var i = length + do { + i-- + } while (i >= 0 && get(i).isWhitespace()) + return subSequence(0, i + 1) as Spanned +} + +/** + * BidiFormatter.unicodeWrap is insufficient in some cases (see #1921) + * So we force isolation manually + * https://unicode.org/reports/tr9/#Explicit_Directional_Isolates + */ +fun CharSequence.unicodeWrap(): String { + return "\u2068${this}\u2069" +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.kt new file mode 100644 index 0000000..08aa992 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ThemeUtils.kt @@ -0,0 +1,70 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ +@file:JvmName("ThemeUtils") + +package com.keylesspalace.tusky.util + +import android.content.Context +import android.content.res.Configuration +import android.graphics.Color +import android.graphics.drawable.Drawable +import androidx.annotation.AttrRes +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.content.res.use +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.settings.AppTheme + +/** + * Provides runtime compatibility to obtain theme information and re-theme views, especially where + * the ability to do so is not supported in resource files. + */ + +fun getDimension(context: Context, @AttrRes attribute: Int): Int { + return context.obtainStyledAttributes(intArrayOf(attribute)).use { array -> + array.getDimensionPixelSize(0, -1) + } +} + +fun setDrawableTint(context: Context, drawable: Drawable, @AttrRes attribute: Int) { + drawable.setTint(MaterialColors.getColor(context, attribute, Color.BLACK)) +} + +fun setAppNightMode(flavor: String?) { + when (flavor) { + AppTheme.NIGHT.value, AppTheme.BLACK.value -> AppCompatDelegate.setDefaultNightMode( + AppCompatDelegate.MODE_NIGHT_YES + ) + AppTheme.DAY.value -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) + AppTheme.AUTO.value -> AppCompatDelegate.setDefaultNightMode( + AppCompatDelegate.MODE_NIGHT_AUTO_TIME + ) + AppTheme.AUTO_SYSTEM.value, AppTheme.AUTO_SYSTEM_BLACK.value -> AppCompatDelegate.setDefaultNightMode( + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + ) + else -> AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM) + } +} + +fun isBlack(config: Configuration, theme: String?): Boolean { + return when (theme) { + AppTheme.BLACK.value -> true + AppTheme.AUTO_SYSTEM_BLACK.value -> when (config.uiMode and Configuration.UI_MODE_NIGHT_MASK) { + Configuration.UI_MODE_NIGHT_NO -> false + Configuration.UI_MODE_NIGHT_YES -> true + else -> false + } + else -> false + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ThrowableExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/ThrowableExtensions.kt new file mode 100644 index 0000000..8cf894d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ThrowableExtensions.kt @@ -0,0 +1,44 @@ +package com.keylesspalace.tusky.util + +import android.content.Context +import com.keylesspalace.tusky.R +import java.io.IOException +import org.json.JSONException +import org.json.JSONObject +import retrofit2.HttpException + +/** + * checks if this throwable indicates an error causes by a 4xx/5xx server response and + * tries to retrieve the error message the server sent + * @return the error message, or null if this is no server error or it had no error message + */ +fun Throwable.getServerErrorMessage(): String? { + if (this is HttpException) { + val errorResponse = response()?.errorBody()?.string() + return if (!errorResponse.isNullOrBlank()) { + try { + JSONObject(errorResponse).getString("error") + } catch (e: JSONException) { + null + } + } else { + null + } + } + return null +} + +/** @return A drawable resource to accompany the error message for this throwable */ +fun Throwable.getDrawableRes(): Int = when (this) { + is IOException -> R.drawable.errorphant_offline + is HttpException -> R.drawable.errorphant_offline + else -> R.drawable.errorphant_error +} + +/** @return A string error message for this throwable */ +fun Throwable.getErrorString(context: Context): String = getServerErrorMessage() ?: when (this) { + is IOException -> context.getString(R.string.error_network) + else -> context.getString(R.string.error_generic) +} + +fun Throwable.isHttpNotFound(): Boolean = (this as? HttpException)?.code() == 404 diff --git a/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.kt new file mode 100644 index 0000000..a6717ed --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/TimestampUtils.kt @@ -0,0 +1,102 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ +@file:JvmName("TimestampUtils") + +package com.keylesspalace.tusky.util + +import android.content.Context +import com.keylesspalace.tusky.R +import kotlin.math.abs + +private const val SECOND_IN_MILLIS: Long = 1000 +private const val MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60 +private const val HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60 +private const val DAY_IN_MILLIS = HOUR_IN_MILLIS * 24 +private const val YEAR_IN_MILLIS = DAY_IN_MILLIS * 365 + +/** + * This is a rough duplicate of [android.text.format.DateUtils.getRelativeTimeSpanString], + * but even with the FORMAT_ABBREV_RELATIVE flag it wasn't abbreviating enough. + */ +fun getRelativeTimeSpanString(context: Context, then: Long, now: Long): String { + var span = now - then + var future = false + if (abs(span) < SECOND_IN_MILLIS) { + return context.getString(R.string.status_created_at_now) + } else if (span < 0) { + future = true + span = -span + } + val format: Int + if (span < MINUTE_IN_MILLIS) { + span /= SECOND_IN_MILLIS + format = if (future) { + R.string.abbreviated_in_seconds + } else { + R.string.abbreviated_seconds_ago + } + } else if (span < HOUR_IN_MILLIS) { + span /= MINUTE_IN_MILLIS + format = if (future) { + R.string.abbreviated_in_minutes + } else { + R.string.abbreviated_minutes_ago + } + } else if (span < DAY_IN_MILLIS) { + span /= HOUR_IN_MILLIS + format = if (future) { + R.string.abbreviated_in_hours + } else { + R.string.abbreviated_hours_ago + } + } else if (span < YEAR_IN_MILLIS) { + span /= DAY_IN_MILLIS + format = if (future) { + R.string.abbreviated_in_days + } else { + R.string.abbreviated_days_ago + } + } else { + span /= YEAR_IN_MILLIS + format = if (future) { + R.string.abbreviated_in_years + } else { + R.string.abbreviated_years_ago + } + } + return context.getString(format, span) +} + +fun formatPollDuration(context: Context, then: Long, now: Long): String { + var span = then - now + if (span < 0) { + span = 0 + } + val format: Int + if (span < MINUTE_IN_MILLIS) { + span /= SECOND_IN_MILLIS + format = R.plurals.poll_timespan_seconds + } else if (span < HOUR_IN_MILLIS) { + span /= MINUTE_IN_MILLIS + format = R.plurals.poll_timespan_minutes + } else if (span < DAY_IN_MILLIS) { + span /= HOUR_IN_MILLIS + format = R.plurals.poll_timespan_hours + } else { + span /= DAY_IN_MILLIS + format = R.plurals.poll_timespan_days + } + return context.resources.getQuantityString(format, span.toInt(), span.toInt()) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/TouchDelegateHelper.kt b/app/src/main/java/com/keylesspalace/tusky/util/TouchDelegateHelper.kt new file mode 100644 index 0000000..a141016 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/TouchDelegateHelper.kt @@ -0,0 +1,64 @@ +/* Copyright 2022 Tusky contributors + + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +@file:JvmName("TouchDelegateHelper") + +package com.keylesspalace.tusky.util + +import android.graphics.Rect +import android.view.MotionEvent +import android.view.TouchDelegate +import android.view.View +import android.view.ViewGroup + +/** + * Expands the touch area of the give row of views to fill the space in-between them, using a + * [TouchDelegate]. + */ +fun ViewGroup.expandTouchSizeToFillRow(children: List<View>) { + addOnLayoutChangeListener { _, _, _, _, _, _, _, _, _ -> + touchDelegate = CompositeTouchDelegate( + this, + children.mapIndexed { i, view -> + val rect = Rect() + view.getHitRect(rect) + val left = children.getOrNull(i - 1) + if (left != null) { + // extend half-way to previous view + rect.left -= (view.left - left.right) / 2 + } + val right = children.getOrNull(i + 1) + if (right != null) { + // extend half-way to next view + rect.right += (right.left - view.right) / 2 + } + TouchDelegate(rect, view) + } + ) + } +} + +private class CompositeTouchDelegate(view: View, private val delegates: List<TouchDelegate>) : + TouchDelegate(Rect(), view) { + + override fun onTouchEvent(event: MotionEvent): Boolean { + val x = event.x + val y = event.y + return delegates.fold(false) { res, delegate -> + event.setLocation(x, y) + delegate.onTouchEvent(event) || res + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt new file mode 100644 index 0000000..5cdcd89 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewBindingExtensions.kt @@ -0,0 +1,51 @@ +package com.keylesspalace.tusky.util + +import android.view.LayoutInflater +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import androidx.fragment.app.Fragment +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.LifecycleOwner +import androidx.viewbinding.ViewBinding + +/** + * Original code: https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c + * Refactor: https://bladecoder.medium.com/viewlifecyclelazy-and-other-ways-to-avoid-view-memory-leaks-in-android-fragments-4aa982e6e579 + */ + +inline fun <T : ViewBinding> AppCompatActivity.viewBinding( + crossinline bindingInflater: (LayoutInflater) -> T +) = lazy(LazyThreadSafetyMode.NONE) { + bindingInflater(layoutInflater) +} + +private class ViewLifecycleLazy<T : Any>( + private val fragment: Fragment, + private val initializer: (View) -> T +) : Lazy<T>, LifecycleEventObserver { + private var cached: T? = null + + override val value: T + get() { + return cached ?: run { + val newValue = initializer(fragment.requireView()) + cached = newValue + fragment.viewLifecycleOwner.lifecycle.addObserver(this) + newValue + } + } + + override fun isInitialized() = cached != null + + override fun toString() = cached.toString() + + override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) { + if (event == Lifecycle.Event.ON_DESTROY) { + cached = null + } + } +} + +fun <T : ViewBinding> Fragment.viewBinding(viewBindingFactory: (View) -> T): Lazy<T> = + ViewLifecycleLazy(this, viewBindingFactory) diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt new file mode 100644 index 0000000..7c0f8ad --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewDataUtils.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +@file:JvmName("ViewDataUtils") + +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.util + +import androidx.paging.CombinedLoadStates +import androidx.paging.LoadState +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.TrendingTag +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.keylesspalace.tusky.viewdata.TranslationViewData +import com.keylesspalace.tusky.viewdata.TrendingViewData + +fun Status.toViewData( + isShowingContent: Boolean, + isExpanded: Boolean, + isCollapsed: Boolean, + isDetailed: Boolean = false, + translation: TranslationViewData? = null, +): StatusViewData.Concrete { + return StatusViewData.Concrete( + status = this, + isShowingContent = isShowingContent, + isCollapsed = isCollapsed, + isExpanded = isExpanded, + isDetailed = isDetailed, + translation = translation, + ) +} + +fun List<TrendingTag>.toViewData(): List<TrendingViewData.Tag> { + val maxTrendingValue = flatMap { tag -> tag.history } + .mapNotNull { it.uses.toLongOrNull() } + .maxOrNull() ?: 1 + + return map { tag -> + + val reversedHistory = tag.history.asReversed() + + TrendingViewData.Tag( + name = tag.name, + usage = reversedHistory.mapNotNull { it.uses.toLongOrNull() }, + accounts = reversedHistory.mapNotNull { it.accounts.toLongOrNull() }, + maxTrendingValue = maxTrendingValue + ) + } +} + +fun CombinedLoadStates.isAnyLoading(): Boolean { + return this.refresh == LoadState.Loading || this.append == LoadState.Loading || this.prepend == LoadState.Loading +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt b/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt new file mode 100644 index 0000000..392f65b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/ViewExtensions.kt @@ -0,0 +1,80 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.util + +import android.util.Log +import android.view.View +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import androidx.viewpager2.widget.ViewPager2 + +fun View.show() { + this.visibility = View.VISIBLE +} + +fun View.hide() { + this.visibility = View.GONE +} + +fun View.visible(visible: Boolean, or: Int = View.GONE) { + this.visibility = if (visible) View.VISIBLE else or +} + +/** + * Reduce ViewPager2's sensitivity to horizontal swipes. + */ +fun ViewPager2.reduceSwipeSensitivity() { + // ViewPager2 is very sensitive to horizontal motion when swiping vertically, and will + // trigger a page transition if the user's swipe is only a few tens of degrees off from + // vertical. This is a problem if the underlying content is a list that the user wants + // to scroll vertically -- it's far too easy to trigger an accidental horizontal swipe. + // + // One way to stop this is to reach in to ViewPager2's RecyclerView and adjust the amount + // of touch slop it has. + // + // See https://issuetracker.google.com/issues/139867645 and + // https://bladecoder.medium.com/fixing-recyclerview-nested-scrolling-in-opposite-direction-f587be5c1a04 + // for more (the approach in that Medium article works, but is still quite sensitive to + // horizontal movement while scrolling). + try { + val recyclerViewField = ViewPager2::class.java.getDeclaredField("mRecyclerView") + recyclerViewField.isAccessible = true + val recyclerView = recyclerViewField.get(this) as RecyclerView + + val touchSlopField = RecyclerView::class.java.getDeclaredField("mTouchSlop") + touchSlopField.isAccessible = true + val touchSlop = touchSlopField.get(recyclerView) as Int + // Experimentally, 2 seems to be a sweet-spot, requiring a downward swipe that's at least + // 45 degrees off the vertical to trigger a change. This is consistent with maximum angle + // supported to open the nav. drawer. + val scaleFactor = 2 + touchSlopField.set(recyclerView, touchSlop * scaleFactor) + } catch (e: Exception) { + Log.w("reduceSwipeSensitivity", e) + } +} + +/** + * TextViews with an ancestor RecyclerView can forget that they are selectable. Toggling + * calls to [TextView.setTextIsSelectable] fixes this. + * + * @see https://issuetracker.google.com/issues/37095917 + */ +fun TextView.fixTextSelection() { + setTextIsSelectable(false) + post { setTextIsSelectable(true) } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/twittertext/Regex.java b/app/src/main/java/com/keylesspalace/tusky/util/twittertext/Regex.java new file mode 100644 index 0000000..c7b588f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/twittertext/Regex.java @@ -0,0 +1,348 @@ +// Copyright 2018 Twitter, Inc. +// Licensed under the Apache License, Version 2.0 +// http://www.apache.org/licenses/LICENSE-2.0 + +// Tusky changed: slight adaptions for Mastodon compatibility + +package com.keylesspalace.tusky.util.twittertext; + +import java.util.Collection; +import java.util.Iterator; +import java.util.regex.Pattern; +import javax.annotation.Nonnull; + +public class Regex { + + protected Regex() { + } + + private static final String URL_VALID_GTLD = + "(?:(?:" + + join(TldLists.GTLDS) + + ")(?=[^a-z0-9@+-]|$))"; + private static final String URL_VALID_CCTLD = + "(?:(?:" + + join(TldLists.CTLDS) + + ")(?=[^a-z0-9@+-]|$))"; + + private static final String INVALID_CHARACTERS = + "\\uFFFE" + // BOM + "\\uFEFF" + // BOM + "\\uFFFF"; // Special + + private static final String DIRECTIONAL_CHARACTERS = + "\\u061C" + // ARABIC LETTER MARK (ALM) + "\\u200E" + // LEFT-TO-RIGHT MARK (LRM) + "\\u200F" + // RIGHT-TO-LEFT MARK (RLM) + "\\u202A" + // LEFT-TO-RIGHT EMBEDDING (LRE) + "\\u202B" + // RIGHT-TO-LEFT EMBEDDING (RLE) + "\\u202C" + // POP DIRECTIONAL FORMATTING (PDF) + "\\u202D" + // LEFT-TO-RIGHT OVERRIDE (LRO) + "\\u202E" + // RIGHT-TO-LEFT OVERRIDE (RLO) + "\\u2066" + // LEFT-TO-RIGHT ISOLATE (LRI) + "\\u2067" + // RIGHT-TO-LEFT ISOLATE (RLI) + "\\u2068" + // FIRST STRONG ISOLATE (FSI) + "\\u2069"; // POP DIRECTIONAL ISOLATE (PDI) + + + private static final String UNICODE_SPACES = "[" + + "\\u0009-\\u000d" + // # White_Space # Cc [5] <control-0009>..<control-000D> + "\\u0020" + // White_Space # Zs SPACE + "\\u0085" + // White_Space # Cc <control-0085> + "\\u00a0" + // White_Space # Zs NO-BREAK SPACE + "\\u1680" + // White_Space # Zs OGHAM SPACE MARK + "\\u180E" + // White_Space # Zs MONGOLIAN VOWEL SEPARATOR + "\\u2000-\\u200a" + // # White_Space # Zs [11] EN QUAD..HAIR SPACE + "\\u2028" + // White_Space # Zl LINE SEPARATOR + "\\u2029" + // White_Space # Zp PARAGRAPH SEPARATOR + "\\u202F" + // White_Space # Zs NARROW NO-BREAK SPACE + "\\u205F" + // White_Space # Zs MEDIUM MATHEMATICAL SPACE + "\\u3000" + // White_Space # Zs IDEOGRAPHIC SPACE + "]"; + + private static final String LATIN_ACCENTS_CHARS = + // Latin-1 + "\\u00c0-\\u00d6\\u00d8-\\u00f6\\u00f8-\\u00ff" + + // Latin Extended A and B + "\\u0100-\\u024f" + + // IPA Extensions + "\\u0253\\u0254\\u0256\\u0257\\u0259\\u025b\\u0263\\u0268\\u026f\\u0272\\u0289\\u028b" + + // Hawaiian + "\\u02bb" + + // Combining diacritics + "\\u0300-\\u036f" + + // Latin Extended Additional (mostly for Vietnamese) + "\\u1e00-\\u1eff"; + + private static final String CYRILLIC_CHARS = "\\u0400-\\u04ff"; + + // Generated from unicode_regex/unicode_regex_groups.scala, more inclusive than Java's \p{L}\p{M} + private static final String HASHTAG_LETTERS_AND_MARKS = "\\p{L}\\p{M}" + + "\\u037f\\u0528-\\u052f\\u08a0-\\u08b2\\u08e4-\\u08ff\\u0978\\u0980\\u0c00\\u0c34\\u0c81" + + "\\u0d01\\u0ede\\u0edf\\u10c7\\u10cd\\u10fd-\\u10ff\\u16f1-\\u16f8\\u17b4\\u17b5\\u191d" + + "\\u191e\\u1ab0-\\u1abe\\u1bab-\\u1bad\\u1bba-\\u1bbf\\u1cf3-\\u1cf6\\u1cf8\\u1cf9" + + "\\u1de7-\\u1df5\\u2cf2\\u2cf3\\u2d27\\u2d2d\\u2d66\\u2d67\\u9fcc\\ua674-\\ua67b\\ua698" + + "-\\ua69d\\ua69f\\ua792-\\ua79f\\ua7aa-\\ua7ad\\ua7b0\\ua7b1\\ua7f7-\\ua7f9\\ua9e0-" + + "\\ua9ef\\ua9fa-\\ua9fe\\uaa7c-\\uaa7f\\uaae0-\\uaaef\\uaaf2-\\uaaf6\\uab30-\\uab5a" + + "\\uab5c-\\uab5f\\uab64\\uab65\\uf870-\\uf87f\\uf882\\uf884-\\uf89f\\uf8b8\\uf8c1-" + + "\\uf8d6\\ufa2e\\ufa2f\\ufe27-\\ufe2d\\ud800\\udee0\\ud800\\udf1f\\ud800\\udf50-\\ud800" + + "\\udf7a\\ud801\\udd00-\\ud801\\udd27\\ud801\\udd30-\\ud801\\udd63\\ud801\\ude00-\\ud801" + + "\\udf36\\ud801\\udf40-\\ud801\\udf55\\ud801\\udf60-\\ud801\\udf67\\ud802\\udc60-\\ud802" + + "\\udc76\\ud802\\udc80-\\ud802\\udc9e\\ud802\\udd80-\\ud802\\uddb7\\ud802\\uddbe\\ud802" + + "\\uddbf\\ud802\\ude80-\\ud802\\ude9c\\ud802\\udec0-\\ud802\\udec7\\ud802\\udec9-\\ud802" + + "\\udee6\\ud802\\udf80-\\ud802\\udf91\\ud804\\udc7f\\ud804\\udcd0-\\ud804\\udce8\\ud804" + + "\\udd00-\\ud804\\udd34\\ud804\\udd50-\\ud804\\udd73\\ud804\\udd76\\ud804\\udd80-\\ud804" + + "\\uddc4\\ud804\\uddda\\ud804\\ude00-\\ud804\\ude11\\ud804\\ude13-\\ud804\\ude37\\ud804" + + "\\udeb0-\\ud804\\udeea\\ud804\\udf01-\\ud804\\udf03\\ud804\\udf05-\\ud804\\udf0c\\ud804" + + "\\udf0f\\ud804\\udf10\\ud804\\udf13-\\ud804\\udf28\\ud804\\udf2a-\\ud804\\udf30\\ud804" + + "\\udf32\\ud804\\udf33\\ud804\\udf35-\\ud804\\udf39\\ud804\\udf3c-\\ud804\\udf44\\ud804" + + "\\udf47\\ud804\\udf48\\ud804\\udf4b-\\ud804\\udf4d\\ud804\\udf57\\ud804\\udf5d-\\ud804" + + "\\udf63\\ud804\\udf66-\\ud804\\udf6c\\ud804\\udf70-\\ud804\\udf74\\ud805\\udc80-\\ud805" + + "\\udcc5\\ud805\\udcc7\\ud805\\udd80-\\ud805\\uddb5\\ud805\\uddb8-\\ud805\\uddc0\\ud805" + + "\\ude00-\\ud805\\ude40\\ud805\\ude44\\ud805\\ude80-\\ud805\\udeb7\\ud806\\udca0-\\ud806" + + "\\udcdf\\ud806\\udcff\\ud806\\udec0-\\ud806\\udef8\\ud808\\udf6f-\\ud808\\udf98\\ud81a" + + "\\ude40-\\ud81a\\ude5e\\ud81a\\uded0-\\ud81a\\udeed\\ud81a\\udef0-\\ud81a\\udef4\\ud81a" + + "\\udf00-\\ud81a\\udf36\\ud81a\\udf40-\\ud81a\\udf43\\ud81a\\udf63-\\ud81a\\udf77\\ud81a" + + "\\udf7d-\\ud81a\\udf8f\\ud81b\\udf00-\\ud81b\\udf44\\ud81b\\udf50-\\ud81b\\udf7e\\ud81b" + + "\\udf8f-\\ud81b\\udf9f\\ud82f\\udc00-\\ud82f\\udc6a\\ud82f\\udc70-\\ud82f\\udc7c\\ud82f" + + "\\udc80-\\ud82f\\udc88\\ud82f\\udc90-\\ud82f\\udc99\\ud82f\\udc9d\\ud82f\\udc9e\\ud83a" + + "\\udc00-\\ud83a\\udcc4\\ud83a\\udcd0-\\ud83a\\udcd6\\ud83b\\ude00-\\ud83b\\ude03\\ud83b" + + "\\ude05-\\ud83b\\ude1f\\ud83b\\ude21\\ud83b\\ude22\\ud83b\\ude24\\ud83b\\ude27\\ud83b" + + "\\ude29-\\ud83b\\ude32\\ud83b\\ude34-\\ud83b\\ude37\\ud83b\\ude39\\ud83b\\ude3b\\ud83b" + + "\\ude42\\ud83b\\ude47\\ud83b\\ude49\\ud83b\\ude4b\\ud83b\\ude4d-\\ud83b\\ude4f\\ud83b" + + "\\ude51\\ud83b\\ude52\\ud83b\\ude54\\ud83b\\ude57\\ud83b\\ude59\\ud83b\\ude5b\\ud83b" + + "\\ude5d\\ud83b\\ude5f\\ud83b\\ude61\\ud83b\\ude62\\ud83b\\ude64\\ud83b\\ude67-\\ud83b" + + "\\ude6a\\ud83b\\ude6c-\\ud83b\\ude72\\ud83b\\ude74-\\ud83b\\ude77\\ud83b\\ude79-\\ud83b" + + "\\ude7c\\ud83b\\ude7e\\ud83b\\ude80-\\ud83b\\ude89\\ud83b\\ude8b-\\ud83b\\ude9b\\ud83b" + + "\\udea1-\\ud83b\\udea3\\ud83b\\udea5-\\ud83b\\udea9\\ud83b\\udeab-\\ud83b\\udebb"; + + // Generated from unicode_regex/unicode_regex_groups.scala, more inclusive than Java's \p{Nd} + private static final String HASHTAG_NUMERALS = "\\p{Nd}" + + "\\u0de6-\\u0def\\ua9f0-\\ua9f9\\ud804\\udcf0-\\ud804\\udcf9\\ud804\\udd36-\\ud804" + + "\\udd3f\\ud804\\uddd0-\\ud804\\uddd9\\ud804\\udef0-\\ud804\\udef9\\ud805\\udcd0-\\ud805" + + "\\udcd9\\ud805\\ude50-\\ud805\\ude59\\ud805\\udec0-\\ud805\\udec9\\ud806\\udce0-\\ud806" + + "\\udce9\\ud81a\\ude60-\\ud81a\\ude69\\ud81a\\udf50-\\ud81a\\udf59"; + + private static final String HASHTAG_SPECIAL_CHARS = "_" + //underscore + "\\u200c" + // ZERO WIDTH NON-JOINER (ZWNJ) + "\\u200d" + // ZERO WIDTH JOINER (ZWJ) + "\\ua67e" + // CYRILLIC KAVYKA + "\\u05be" + // HEBREW PUNCTUATION MAQAF + "\\u05f3" + // HEBREW PUNCTUATION GERESH + "\\u05f4" + // HEBREW PUNCTUATION GERSHAYIM + "\\uff5e" + // FULLWIDTH TILDE + "\\u301c" + // WAVE DASH + "\\u309b" + // KATAKANA-HIRAGANA VOICED SOUND MARK + "\\u309c" + // KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK + "\\u30a0" + // KATAKANA-HIRAGANA DOUBLE HYPHEN + "\\u30fb" + // KATAKANA MIDDLE DOT + "\\u3003" + // DITTO MARK + "\\u0f0b" + // TIBETAN MARK INTERSYLLABIC TSHEG + "\\u0f0c" + // TIBETAN MARK DELIMITER TSHEG BSTAR + "\\u00b7"; // MIDDLE DOT + + private static final String HASHTAG_LETTERS_NUMERALS = + HASHTAG_LETTERS_AND_MARKS + HASHTAG_NUMERALS + HASHTAG_SPECIAL_CHARS; + private static final String HASHTAG_LETTERS_SET = "[" + HASHTAG_LETTERS_AND_MARKS + "]"; + private static final String HASHTAG_LETTERS_NUMERALS_SET = "[" + HASHTAG_LETTERS_NUMERALS + "]"; + + /* URL related hash regex collection */ + private static final String URL_VALID_PRECEDING_CHARS = + "(?:[^a-z0-9@@$##" + INVALID_CHARACTERS + "]|[" + DIRECTIONAL_CHARACTERS + "]|^)"; + + private static final String URL_VALID_CHARS = "[a-z0-9" + LATIN_ACCENTS_CHARS + "]"; + private static final String URL_VALID_SUBDOMAIN = + "(?>(?:" + URL_VALID_CHARS + "[" + URL_VALID_CHARS + "\\-_]*)?" + URL_VALID_CHARS + "\\.)"; + private static final String URL_VALID_DOMAIN_NAME = + "(?:(?:" + URL_VALID_CHARS + "[" + URL_VALID_CHARS + "\\-]*)?" + URL_VALID_CHARS + "\\.)"; + + private static final String PUNCTUATION_CHARS = "-_!\"#$%&'\\(\\)*+,./:;<=>?@\\[\\]^`\\{|}~"; + + // Any non-space, non-punctuation characters. + // \p{Z} = any kind of whitespace or invisible separator. + private static final String URL_VALID_UNICODE_CHARS = + "[^" + PUNCTUATION_CHARS + "\\s\\p{Z}\\p{InGeneralPunctuation}]"; + private static final String URL_VALID_UNICODE_DOMAIN_NAME = + "(?:(?:" + URL_VALID_UNICODE_CHARS + "[" + URL_VALID_UNICODE_CHARS + "\\-]*)?" + + URL_VALID_UNICODE_CHARS + "\\.)"; + + private static final String URL_PUNYCODE = "(?:xn--[-0-9a-z]+)"; + + private static final String URL_VALID_DOMAIN = + "(?:" + // optional sub-domain + domain + TLD + URL_VALID_SUBDOMAIN + "*" + URL_VALID_DOMAIN_NAME + // e.g. twitter.com, foo.co.jp ... + "(?:" + URL_VALID_GTLD + "|" + URL_VALID_CCTLD + "|" + URL_PUNYCODE + ")" + + ")" + + "|(?:" + "(?<=https?://)" + + "(?:" + + "(?:" + URL_VALID_DOMAIN_NAME + URL_VALID_CCTLD + ")" + // protocol + domain + ccTLD + "|(?:" + + URL_VALID_UNICODE_DOMAIN_NAME + // protocol + unicode domain + TLD + "(?:" + URL_VALID_GTLD + "|" + URL_VALID_CCTLD + ")" + + ")" + + ")" + + ")" + + "|(?:" + // domain + ccTLD + '/' + URL_VALID_DOMAIN_NAME + URL_VALID_CCTLD + "(?=/)" + // e.g. t.co/ + ")"; + + private static final String URL_VALID_PORT_NUMBER = "[0-9]++"; + + private static final String URL_VALID_GENERAL_PATH_CHARS = + "[a-z0-9!\\*';:=\\+,.\\$/%#\\[\\]\\-\\u2013_~\\|&@" + + LATIN_ACCENTS_CHARS + CYRILLIC_CHARS + "]"; + + /** + * Allow URL paths to contain up to two nested levels of balanced parens + * 1. Used in Wikipedia URLs like /Primer_(film) + * 2. Used in IIS sessions like /S(dfd346)/ + * 3. Used in Rdio URLs like /track/We_Up_(Album_Version_(Edited))/ + */ + private static final String URL_BALANCED_PARENS = "\\(" + + "(?:" + + URL_VALID_GENERAL_PATH_CHARS + "+" + + "|" + + // allow one nested level of balanced parentheses + "(?:" + + URL_VALID_GENERAL_PATH_CHARS + "*" + + "\\(" + + URL_VALID_GENERAL_PATH_CHARS + "+" + + "\\)" + + URL_VALID_GENERAL_PATH_CHARS + "*" + + ")" + + ")" + + "\\)"; + + /** + * Valid end-of-path characters (so /foo. does not gobble the period). + * 2. Allow =&# for empty URL parameters and other URL-join artifacts + */ + private static final String URL_VALID_PATH_ENDING_CHARS = + "[a-z0-9=_#/\\-\\+" + LATIN_ACCENTS_CHARS + CYRILLIC_CHARS + "]|(?:" + + URL_BALANCED_PARENS + ")"; + + private static final String URL_VALID_PATH = "(?:" + + "(?:" + + URL_VALID_GENERAL_PATH_CHARS + "*" + + "(?:" + URL_BALANCED_PARENS + URL_VALID_GENERAL_PATH_CHARS + "*)*" + + URL_VALID_PATH_ENDING_CHARS + + ")|(?:@" + URL_VALID_GENERAL_PATH_CHARS + "+/)" + + ")"; + + private static final String URL_VALID_URL_QUERY_CHARS = + "[a-z0-9!?\\*'\\(\\);:&=\\+\\$/%#\\[\\]\\-_\\.,~\\|@]"; + private static final String URL_VALID_URL_QUERY_ENDING_CHARS = "[a-z0-9\\-_&=#/]"; + public static final String VALID_URL_PATTERN_STRING = + URL_VALID_PRECEDING_CHARS + + "(" + + "https?://" + + "(" + URL_VALID_DOMAIN + ")" + + "(?::(" + URL_VALID_PORT_NUMBER + "))?" + + "(/" + + URL_VALID_PATH + "*+" + + ")?" + + "(\\?" + URL_VALID_URL_QUERY_CHARS + "*" + + URL_VALID_URL_QUERY_ENDING_CHARS + ")?" + + ")"; + + private static final String AT_SIGNS_CHARS = "@\uFF20"; + private static final String DOLLAR_SIGN_CHAR = "\\$"; + private static final String CASHTAG = "[a-z]{1,6}(?:[._][a-z]{1,2})?"; + + /* Begin public constants */ + + public static final Pattern INVALID_CHARACTERS_PATTERN; + public static final Pattern VALID_HASHTAG; + public static final int VALID_HASHTAG_GROUP_BEFORE = 1; + public static final int VALID_HASHTAG_GROUP_HASH = 2; + public static final int VALID_HASHTAG_GROUP_TAG = 3; + public static final Pattern INVALID_HASHTAG_MATCH_END; + public static final Pattern RTL_CHARACTERS; + + public static final Pattern AT_SIGNS; + public static final Pattern VALID_MENTION_OR_LIST; + public static final int VALID_MENTION_OR_LIST_GROUP_BEFORE = 1; + public static final int VALID_MENTION_OR_LIST_GROUP_AT = 2; + public static final int VALID_MENTION_OR_LIST_GROUP_USERNAME = 3; + public static final int VALID_MENTION_OR_LIST_GROUP_LIST = 4; + + public static final Pattern VALID_REPLY; + public static final int VALID_REPLY_GROUP_USERNAME = 1; + + public static final Pattern INVALID_MENTION_MATCH_END; + + /** + * Regex to extract URL (it also includes the text preceding the url). + * + * This regex does not reflect its name and {@link Regex#VALID_URL_GROUP_URL} match + * should be checked in order to match a valid url. This is not ideal, but the behavior is + * being kept to ensure backwards compatibility. Ideally this regex should be + * implemented with a negative lookbehind as opposed to a negated character class + * but lack of JS support increases maint overhead if the logic is different by + * platform. + */ + + public static final Pattern VALID_URL; + public static final int VALID_URL_GROUP_ALL = 1; + public static final int VALID_URL_GROUP_BEFORE = 2; + public static final int VALID_URL_GROUP_URL = 3; + public static final int VALID_URL_GROUP_PROTOCOL = 4; + public static final int VALID_URL_GROUP_DOMAIN = 5; + public static final int VALID_URL_GROUP_PORT = 6; + public static final int VALID_URL_GROUP_PATH = 7; + public static final int VALID_URL_GROUP_QUERY_STRING = 8; + + public static final Pattern VALID_TCO_URL; + public static final Pattern INVALID_URL_WITHOUT_PROTOCOL_MATCH_BEGIN; + + public static final Pattern VALID_CASHTAG; + public static final int VALID_CASHTAG_GROUP_BEFORE = 1; + public static final int VALID_CASHTAG_GROUP_DOLLAR = 2; + public static final int VALID_CASHTAG_GROUP_CASHTAG = 3; + + public static final Pattern VALID_DOMAIN; + + // initializing in a static synchronized block, + // there appears to be thread safety issues with Pattern.compile in android + static { + synchronized (Regex.class) { + INVALID_CHARACTERS_PATTERN = Pattern.compile(".*[" + INVALID_CHARACTERS + "].*"); + VALID_HASHTAG = Pattern.compile("(^|\\uFE0E|\\uFE0F|[^&" + HASHTAG_LETTERS_NUMERALS + + "])([#\uFF03])(?![\uFE0F\u20E3])(" + HASHTAG_LETTERS_NUMERALS_SET + "*" + + HASHTAG_LETTERS_SET + HASHTAG_LETTERS_NUMERALS_SET + "*)", Pattern.CASE_INSENSITIVE); + INVALID_HASHTAG_MATCH_END = Pattern.compile("^(?:[##]|://)"); + RTL_CHARACTERS = Pattern.compile("[\u0600-\u06FF\u0750-\u077F\u0590-\u05FF\uFE70-\uFEFF]"); + AT_SIGNS = Pattern.compile("[" + AT_SIGNS_CHARS + "]"); + VALID_MENTION_OR_LIST = Pattern.compile("([^a-z0-9_!#$%&*" + AT_SIGNS_CHARS + + "]|^|(?:^|[^a-z0-9_+~.-])RT:?)(" + AT_SIGNS + + "+)([a-z0-9_]{1,20})(/[a-z][a-z0-9_\\-]{0,24})?", Pattern.CASE_INSENSITIVE); + VALID_REPLY = Pattern.compile("^(?:" + UNICODE_SPACES + "|" + DIRECTIONAL_CHARACTERS + ")*" + + AT_SIGNS + "([a-z0-9_]{1,20})", Pattern.CASE_INSENSITIVE); + INVALID_MENTION_MATCH_END = + Pattern.compile("^(?:[" + AT_SIGNS_CHARS + LATIN_ACCENTS_CHARS + "]|://)"); + INVALID_URL_WITHOUT_PROTOCOL_MATCH_BEGIN = Pattern.compile("[-_./]$"); + + VALID_URL = Pattern.compile(VALID_URL_PATTERN_STRING, Pattern.CASE_INSENSITIVE); + VALID_TCO_URL = Pattern.compile("^https?://t\\.co/([a-z0-9]+)(?:\\?" + + URL_VALID_URL_QUERY_CHARS + "*" + URL_VALID_URL_QUERY_ENDING_CHARS + ")?", + Pattern.CASE_INSENSITIVE); + VALID_CASHTAG = Pattern.compile("(^|" + UNICODE_SPACES + "|" + DIRECTIONAL_CHARACTERS + ")(" + + DOLLAR_SIGN_CHAR + ")(" + CASHTAG + ")" + "(?=$|\\s|\\p{Punct})", + Pattern.CASE_INSENSITIVE); + VALID_DOMAIN = Pattern.compile(URL_VALID_DOMAIN, Pattern.CASE_INSENSITIVE); + } + } + + private static String join(@Nonnull Collection<?> col) { + final StringBuilder sb = new StringBuilder(); + final Iterator<?> iter = col.iterator(); + if (iter.hasNext()) { + sb.append(iter.next().toString()); + } + while (iter.hasNext()) { + sb.append("|"); + sb.append(iter.next().toString()); + } + return sb.toString(); + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/util/twittertext/TldLists.java b/app/src/main/java/com/keylesspalace/tusky/util/twittertext/TldLists.java new file mode 100644 index 0000000..220bdcf --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/util/twittertext/TldLists.java @@ -0,0 +1,1593 @@ +// Copyright 2018 Twitter, Inc. +// Licensed under the Apache License, Version 2.0 +// http://www.apache.org/licenses/LICENSE-2.0 + +package com.keylesspalace.tusky.util.twittertext; + +import java.util.Arrays; +import java.util.List; + +public final class TldLists { + private TldLists() { + } + + public static final List<String> GTLDS = Arrays.asList( + "삼성", + "닷컴", + "닷넷", + "香格里拉", + "餐厅", + "食品", + "飞利浦", + "電訊盈科", + "集团", + "通販", + "购物", + "谷歌", + "诺基亚", + "联通", + "网络", + "网站", + "网店", + "网址", + "组织机构", + "移动", + "珠宝", + "点看", + "游戏", + "淡马锡", + "机构", + "書籍", + "时尚", + "新闻", + "政府", + "政务", + "招聘", + "手表", + "手机", + "我爱你", + "慈善", + "微博", + "广东", + "工行", + "家電", + "娱乐", + "天主教", + "大拿", + "大众汽车", + "在线", + "嘉里大酒店", + "嘉里", + "商标", + "商店", + "商城", + "公益", + "公司", + "八卦", + "健康", + "信息", + "佛山", + "企业", + "中文网", + "中信", + "世界", + "ポイント", + "ファッション", + "セール", + "ストア", + "コム", + "グーグル", + "クラウド", + "みんな", + "คอม", + "संगठन", + "नेट", + "कॉम", + "همراه", + "موقع", + "موبايلي", + "كوم", + "كاثوليك", + "عرب", + "شبكة", + "بيتك", + "بازار", + "العليان", + "ارامكو", + "اتصالات", + "ابوظبي", + "קום", + "сайт", + "рус", + "орг", + "онлайн", + "москва", + "ком", + "католик", + "дети", + "zuerich", + "zone", + "zippo", + "zip", + "zero", + "zara", + "zappos", + "yun", + "youtube", + "you", + "yokohama", + "yoga", + "yodobashi", + "yandex", + "yamaxun", + "yahoo", + "yachts", + "xyz", + "xxx", + "xperia", + "xin", + "xihuan", + "xfinity", + "xerox", + "xbox", + "wtf", + "wtc", + "wow", + "world", + "works", + "work", + "woodside", + "wolterskluwer", + "wme", + "winners", + "wine", + "windows", + "win", + "williamhill", + "wiki", + "wien", + "whoswho", + "weir", + "weibo", + "wedding", + "wed", + "website", + "weber", + "webcam", + "weatherchannel", + "weather", + "watches", + "watch", + "warman", + "wanggou", + "wang", + "walter", + "walmart", + "wales", + "vuelos", + "voyage", + "voto", + "voting", + "vote", + "volvo", + "volkswagen", + "vodka", + "vlaanderen", + "vivo", + "viva", + "vistaprint", + "vista", + "vision", + "visa", + "virgin", + "vip", + "vin", + "villas", + "viking", + "vig", + "video", + "viajes", + "vet", + "versicherung", + "vermögensberatung", + "vermögensberater", + "verisign", + "ventures", + "vegas", + "vanguard", + "vana", + "vacations", + "ups", + "uol", + "uno", + "university", + "unicom", + "uconnect", + "ubs", + "ubank", + "tvs", + "tushu", + "tunes", + "tui", + "tube", + "trv", + "trust", + "travelersinsurance", + "travelers", + "travelchannel", + "travel", + "training", + "trading", + "trade", + "toys", + "toyota", + "town", + "tours", + "total", + "toshiba", + "toray", + "top", + "tools", + "tokyo", + "today", + "tmall", + "tkmaxx", + "tjx", + "tjmaxx", + "tirol", + "tires", + "tips", + "tiffany", + "tienda", + "tickets", + "tiaa", + "theatre", + "theater", + "thd", + "teva", + "tennis", + "temasek", + "telefonica", + "telecity", + "tel", + "technology", + "tech", + "team", + "tdk", + "tci", + "taxi", + "tax", + "tattoo", + "tatar", + "tatamotors", + "target", + "taobao", + "talk", + "taipei", + "tab", + "systems", + "symantec", + "sydney", + "swiss", + "swiftcover", + "swatch", + "suzuki", + "surgery", + "surf", + "support", + "supply", + "supplies", + "sucks", + "style", + "study", + "studio", + "stream", + "store", + "storage", + "stockholm", + "stcgroup", + "stc", + "statoil", + "statefarm", + "statebank", + "starhub", + "star", + "staples", + "stada", + "srt", + "srl", + "spreadbetting", + "spot", + "sport", + "spiegel", + "space", + "soy", + "sony", + "song", + "solutions", + "solar", + "sohu", + "software", + "softbank", + "social", + "soccer", + "sncf", + "smile", + "smart", + "sling", + "skype", + "sky", + "skin", + "ski", + "site", + "singles", + "sina", + "silk", + "shriram", + "showtime", + "show", + "shouji", + "shopping", + "shop", + "shoes", + "shiksha", + "shia", + "shell", + "shaw", + "sharp", + "shangrila", + "sfr", + "sexy", + "sex", + "sew", + "seven", + "ses", + "services", + "sener", + "select", + "seek", + "security", + "secure", + "seat", + "search", + "scot", + "scor", + "scjohnson", + "science", + "schwarz", + "schule", + "school", + "scholarships", + "schmidt", + "schaeffler", + "scb", + "sca", + "sbs", + "sbi", + "saxo", + "save", + "sas", + "sarl", + "sapo", + "sap", + "sanofi", + "sandvikcoromant", + "sandvik", + "samsung", + "samsclub", + "salon", + "sale", + "sakura", + "safety", + "safe", + "saarland", + "ryukyu", + "rwe", + "run", + "ruhr", + "rugby", + "rsvp", + "room", + "rogers", + "rodeo", + "rocks", + "rocher", + "rmit", + "rip", + "rio", + "ril", + "rightathome", + "ricoh", + "richardli", + "rich", + "rexroth", + "reviews", + "review", + "restaurant", + "rest", + "republican", + "report", + "repair", + "rentals", + "rent", + "ren", + "reliance", + "reit", + "reisen", + "reise", + "rehab", + "redumbrella", + "redstone", + "red", + "recipes", + "realty", + "realtor", + "realestate", + "read", + "raid", + "radio", + "racing", + "qvc", + "quest", + "quebec", + "qpon", + "pwc", + "pub", + "prudential", + "pru", + "protection", + "property", + "properties", + "promo", + "progressive", + "prof", + "productions", + "prod", + "pro", + "prime", + "press", + "praxi", + "pramerica", + "post", + "porn", + "politie", + "poker", + "pohl", + "pnc", + "plus", + "plumbing", + "playstation", + "play", + "place", + "pizza", + "pioneer", + "pink", + "ping", + "pin", + "pid", + "pictures", + "pictet", + "pics", + "piaget", + "physio", + "photos", + "photography", + "photo", + "phone", + "philips", + "phd", + "pharmacy", + "pfizer", + "pet", + "pccw", + "pay", + "passagens", + "party", + "parts", + "partners", + "pars", + "paris", + "panerai", + "panasonic", + "pamperedchef", + "page", + "ovh", + "ott", + "otsuka", + "osaka", + "origins", + "orientexpress", + "organic", + "org", + "orange", + "oracle", + "open", + "ooo", + "onyourside", + "online", + "onl", + "ong", + "one", + "omega", + "ollo", + "oldnavy", + "olayangroup", + "olayan", + "okinawa", + "office", + "off", + "observer", + "obi", + "nyc", + "ntt", + "nrw", + "nra", + "nowtv", + "nowruz", + "now", + "norton", + "northwesternmutual", + "nokia", + "nissay", + "nissan", + "ninja", + "nikon", + "nike", + "nico", + "nhk", + "ngo", + "nfl", + "nexus", + "nextdirect", + "next", + "news", + "newholland", + "new", + "neustar", + "network", + "netflix", + "netbank", + "net", + "nec", + "nba", + "navy", + "natura", + "nationwide", + "name", + "nagoya", + "nadex", + "nab", + "mutuelle", + "mutual", + "museum", + "mtr", + "mtpc", + "mtn", + "msd", + "movistar", + "movie", + "mov", + "motorcycles", + "moto", + "moscow", + "mortgage", + "mormon", + "mopar", + "montblanc", + "monster", + "money", + "monash", + "mom", + "moi", + "moe", + "moda", + "mobily", + "mobile", + "mobi", + "mma", + "mls", + "mlb", + "mitsubishi", + "mit", + "mint", + "mini", + "mil", + "microsoft", + "miami", + "metlife", + "merckmsd", + "meo", + "menu", + "men", + "memorial", + "meme", + "melbourne", + "meet", + "media", + "med", + "mckinsey", + "mcdonalds", + "mcd", + "mba", + "mattel", + "maserati", + "marshalls", + "marriott", + "markets", + "marketing", + "market", + "map", + "mango", + "management", + "man", + "makeup", + "maison", + "maif", + "madrid", + "macys", + "luxury", + "luxe", + "lupin", + "lundbeck", + "ltda", + "ltd", + "lplfinancial", + "lpl", + "love", + "lotto", + "lotte", + "london", + "lol", + "loft", + "locus", + "locker", + "loans", + "loan", + "llp", + "llc", + "lixil", + "living", + "live", + "lipsy", + "link", + "linde", + "lincoln", + "limo", + "limited", + "lilly", + "like", + "lighting", + "lifestyle", + "lifeinsurance", + "life", + "lidl", + "liaison", + "lgbt", + "lexus", + "lego", + "legal", + "lefrak", + "leclerc", + "lease", + "lds", + "lawyer", + "law", + "latrobe", + "latino", + "lat", + "lasalle", + "lanxess", + "landrover", + "land", + "lancome", + "lancia", + "lancaster", + "lamer", + "lamborghini", + "ladbrokes", + "lacaixa", + "kyoto", + "kuokgroup", + "kred", + "krd", + "kpn", + "kpmg", + "kosher", + "komatsu", + "koeln", + "kiwi", + "kitchen", + "kindle", + "kinder", + "kim", + "kia", + "kfh", + "kerryproperties", + "kerrylogistics", + "kerryhotels", + "kddi", + "kaufen", + "juniper", + "juegos", + "jprs", + "jpmorgan", + "joy", + "jot", + "joburg", + "jobs", + "jnj", + "jmp", + "jll", + "jlc", + "jio", + "jewelry", + "jetzt", + "jeep", + "jcp", + "jcb", + "java", + "jaguar", + "iwc", + "iveco", + "itv", + "itau", + "istanbul", + "ist", + "ismaili", + "iselect", + "irish", + "ipiranga", + "investments", + "intuit", + "international", + "intel", + "int", + "insure", + "insurance", + "institute", + "ink", + "ing", + "info", + "infiniti", + "industries", + "inc", + "immobilien", + "immo", + "imdb", + "imamat", + "ikano", + "iinet", + "ifm", + "ieee", + "icu", + "ice", + "icbc", + "ibm", + "hyundai", + "hyatt", + "hughes", + "htc", + "hsbc", + "how", + "house", + "hotmail", + "hotels", + "hoteles", + "hot", + "hosting", + "host", + "hospital", + "horse", + "honeywell", + "honda", + "homesense", + "homes", + "homegoods", + "homedepot", + "holiday", + "holdings", + "hockey", + "hkt", + "hiv", + "hitachi", + "hisamitsu", + "hiphop", + "hgtv", + "hermes", + "here", + "helsinki", + "help", + "healthcare", + "health", + "hdfcbank", + "hdfc", + "hbo", + "haus", + "hangout", + "hamburg", + "hair", + "guru", + "guitars", + "guide", + "guge", + "gucci", + "guardian", + "group", + "grocery", + "gripe", + "green", + "gratis", + "graphics", + "grainger", + "gov", + "got", + "gop", + "google", + "goog", + "goodyear", + "goodhands", + "goo", + "golf", + "goldpoint", + "gold", + "godaddy", + "gmx", + "gmo", + "gmbh", + "gmail", + "globo", + "global", + "gle", + "glass", + "glade", + "giving", + "gives", + "gifts", + "gift", + "ggee", + "george", + "genting", + "gent", + "gea", + "gdn", + "gbiz", + "gay", + "garden", + "gap", + "games", + "game", + "gallup", + "gallo", + "gallery", + "gal", + "fyi", + "futbol", + "furniture", + "fund", + "fun", + "fujixerox", + "fujitsu", + "ftr", + "frontier", + "frontdoor", + "frogans", + "frl", + "fresenius", + "free", + "fox", + "foundation", + "forum", + "forsale", + "forex", + "ford", + "football", + "foodnetwork", + "food", + "foo", + "fly", + "flsmidth", + "flowers", + "florist", + "flir", + "flights", + "flickr", + "fitness", + "fit", + "fishing", + "fish", + "firmdale", + "firestone", + "fire", + "financial", + "finance", + "final", + "film", + "fido", + "fidelity", + "fiat", + "ferrero", + "ferrari", + "feedback", + "fedex", + "fast", + "fashion", + "farmers", + "farm", + "fans", + "fan", + "family", + "faith", + "fairwinds", + "fail", + "fage", + "extraspace", + "express", + "exposed", + "expert", + "exchange", + "everbank", + "events", + "eus", + "eurovision", + "etisalat", + "esurance", + "estate", + "esq", + "erni", + "ericsson", + "equipment", + "epson", + "epost", + "enterprises", + "engineering", + "engineer", + "energy", + "emerck", + "email", + "education", + "edu", + "edeka", + "eco", + "eat", + "earth", + "dvr", + "dvag", + "durban", + "dupont", + "duns", + "dunlop", + "duck", + "dubai", + "dtv", + "drive", + "download", + "dot", + "doosan", + "domains", + "doha", + "dog", + "dodge", + "doctor", + "docs", + "dnp", + "diy", + "dish", + "discover", + "discount", + "directory", + "direct", + "digital", + "diet", + "diamonds", + "dhl", + "dev", + "design", + "desi", + "dentist", + "dental", + "democrat", + "delta", + "deloitte", + "dell", + "delivery", + "degree", + "deals", + "dealer", + "deal", + "dds", + "dclk", + "day", + "datsun", + "dating", + "date", + "data", + "dance", + "dad", + "dabur", + "cyou", + "cymru", + "cuisinella", + "csc", + "cruises", + "cruise", + "crs", + "crown", + "cricket", + "creditunion", + "creditcard", + "credit", + "cpa", + "courses", + "coupons", + "coupon", + "country", + "corsica", + "coop", + "cool", + "cookingchannel", + "cooking", + "contractors", + "contact", + "consulting", + "construction", + "condos", + "comsec", + "computer", + "compare", + "company", + "community", + "commbank", + "comcast", + "com", + "cologne", + "college", + "coffee", + "codes", + "coach", + "clubmed", + "club", + "cloud", + "clothing", + "clinique", + "clinic", + "click", + "cleaning", + "claims", + "cityeats", + "city", + "citic", + "citi", + "citadel", + "cisco", + "circle", + "cipriani", + "church", + "chrysler", + "chrome", + "christmas", + "chloe", + "chintai", + "cheap", + "chat", + "chase", + "charity", + "channel", + "chanel", + "cfd", + "cfa", + "cern", + "ceo", + "center", + "ceb", + "cbs", + "cbre", + "cbn", + "cba", + "catholic", + "catering", + "cat", + "casino", + "cash", + "caseih", + "case", + "casa", + "cartier", + "cars", + "careers", + "career", + "care", + "cards", + "caravan", + "car", + "capitalone", + "capital", + "capetown", + "canon", + "cancerresearch", + "camp", + "camera", + "cam", + "calvinklein", + "call", + "cal", + "cafe", + "cab", + "bzh", + "buzz", + "buy", + "business", + "builders", + "build", + "bugatti", + "budapest", + "brussels", + "brother", + "broker", + "broadway", + "bridgestone", + "bradesco", + "box", + "boutique", + "bot", + "boston", + "bostik", + "bosch", + "boots", + "booking", + "book", + "boo", + "bond", + "bom", + "bofa", + "boehringer", + "boats", + "bnpparibas", + "bnl", + "bmw", + "bms", + "blue", + "bloomberg", + "blog", + "blockbuster", + "blanco", + "blackfriday", + "black", + "biz", + "bio", + "bingo", + "bing", + "bike", + "bid", + "bible", + "bharti", + "bet", + "bestbuy", + "best", + "berlin", + "bentley", + "beer", + "beauty", + "beats", + "bcn", + "bcg", + "bbva", + "bbt", + "bbc", + "bayern", + "bauhaus", + "basketball", + "baseball", + "bargains", + "barefoot", + "barclays", + "barclaycard", + "barcelona", + "bar", + "bank", + "band", + "bananarepublic", + "banamex", + "baidu", + "baby", + "azure", + "axa", + "aws", + "avianca", + "autos", + "auto", + "author", + "auspost", + "audio", + "audible", + "audi", + "auction", + "attorney", + "athleta", + "associates", + "asia", + "asda", + "arte", + "art", + "arpa", + "army", + "archi", + "aramco", + "arab", + "aquarelle", + "apple", + "app", + "apartments", + "aol", + "anz", + "anquan", + "android", + "analytics", + "amsterdam", + "amica", + "amfam", + "amex", + "americanfamily", + "americanexpress", + "alstom", + "alsace", + "ally", + "allstate", + "allfinanz", + "alipay", + "alibaba", + "alfaromeo", + "akdn", + "airtel", + "airforce", + "airbus", + "aigo", + "aig", + "agency", + "agakhan", + "africa", + "afl", + "afamilycompany", + "aetna", + "aero", + "aeg", + "adult", + "ads", + "adac", + "actor", + "active", + "aco", + "accountants", + "accountant", + "accenture", + "academy", + "abudhabi", + "abogado", + "able", + "abc", + "abbvie", + "abbott", + "abb", + "abarth", + "aarp", + "aaa", + "onion" + ); + + public static final List<String> CTLDS = Arrays.asList( + "한국", + "香港", + "澳門", + "新加坡", + "台灣", + "台湾", + "中國", + "中国", + "გე", + "ລາວ", + "ไทย", + "ලංකා", + "ഭാരതം", + "ಭಾರತ", + "భారత్", + "சிங்கப்பூர்", + "இலங்கை", + "இந்தியா", + "ଭାରତ", + "ભારત", + "ਭਾਰਤ", + "ভাৰত", + "ভারত", + "বাংলা", + "भारोत", + "भारतम्", + "भारत", + "ڀارت", + "پاکستان", + "موريتانيا", + "مليسيا", + "مصر", + "قطر", + "فلسطين", + "عمان", + "عراق", + "سورية", + "سودان", + "تونس", + "بھارت", + "بارت", + "ایران", + "امارات", + "المغرب", + "السعودية", + "الجزائر", + "البحرين", + "الاردن", + "հայ", + "қаз", + "укр", + "срб", + "рф", + "мон", + "мкд", + "ею", + "бел", + "бг", + "ευ", + "ελ", + "zw", + "zm", + "za", + "yt", + "ye", + "ws", + "wf", + "vu", + "vn", + "vi", + "vg", + "ve", + "vc", + "va", + "uz", + "uy", + "us", + "um", + "uk", + "ug", + "ua", + "tz", + "tw", + "tv", + "tt", + "tr", + "tp", + "to", + "tn", + "tm", + "tl", + "tk", + "tj", + "th", + "tg", + "tf", + "td", + "tc", + "sz", + "sy", + "sx", + "sv", + "su", + "st", + "ss", + "sr", + "so", + "sn", + "sm", + "sl", + "sk", + "sj", + "si", + "sh", + "sg", + "se", + "sd", + "sc", + "sb", + "sa", + "rw", + "ru", + "rs", + "ro", + "re", + "qa", + "py", + "pw", + "pt", + "ps", + "pr", + "pn", + "pm", + "pl", + "pk", + "ph", + "pg", + "pf", + "pe", + "pa", + "om", + "nz", + "nu", + "nr", + "np", + "no", + "nl", + "ni", + "ng", + "nf", + "ne", + "nc", + "na", + "mz", + "my", + "mx", + "mw", + "mv", + "mu", + "mt", + "ms", + "mr", + "mq", + "mp", + "mo", + "mn", + "mm", + "ml", + "mk", + "mh", + "mg", + "mf", + "me", + "md", + "mc", + "ma", + "ly", + "lv", + "lu", + "lt", + "ls", + "lr", + "lk", + "li", + "lc", + "lb", + "la", + "kz", + "ky", + "kw", + "kr", + "kp", + "kn", + "km", + "ki", + "kh", + "kg", + "ke", + "jp", + "jo", + "jm", + "je", + "it", + "is", + "ir", + "iq", + "io", + "in", + "im", + "il", + "ie", + "id", + "hu", + "ht", + "hr", + "hn", + "hm", + "hk", + "gy", + "gw", + "gu", + "gt", + "gs", + "gr", + "gq", + "gp", + "gn", + "gm", + "gl", + "gi", + "gh", + "gg", + "gf", + "ge", + "gd", + "gb", + "ga", + "fr", + "fo", + "fm", + "fk", + "fj", + "fi", + "eu", + "et", + "es", + "er", + "eh", + "eg", + "ee", + "ec", + "dz", + "do", + "dm", + "dk", + "dj", + "de", + "cz", + "cy", + "cx", + "cw", + "cv", + "cu", + "cr", + "co", + "cn", + "cm", + "cl", + "ck", + "ci", + "ch", + "cg", + "cf", + "cd", + "cc", + "ca", + "bz", + "by", + "bw", + "bv", + "bt", + "bs", + "br", + "bq", + "bo", + "bn", + "bm", + "bl", + "bj", + "bi", + "bh", + "bg", + "bf", + "be", + "bd", + "bb", + "ba", + "az", + "ax", + "aw", + "au", + "at", + "as", + "ar", + "aq", + "ao", + "an", + "am", + "al", + "ai", + "ag", + "af", + "ae", + "ad", + "ac" + ); +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/AdaptiveTabLayout.kt b/app/src/main/java/com/keylesspalace/tusky/view/AdaptiveTabLayout.kt new file mode 100644 index 0000000..800165f --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/AdaptiveTabLayout.kt @@ -0,0 +1,41 @@ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.util.AttributeSet +import android.view.ViewGroup +import com.google.android.material.tabs.TabLayout + +/** + * Workaround for "auto" mode not behaving as expected. + * + * Switches the tab display mode depending on available size: start out with "scrollable" but + * if there is enough room switch to "fixed" (and re-measure). + * + * Idea taken from https://stackoverflow.com/a/44894143 + */ +class AdaptiveTabLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : TabLayout(context, attrs, defStyleAttr) { + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + tabMode = MODE_SCROLLABLE // make sure to only measure the "minimum width" + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + if (tabCount < 2) { + return + } + + val tabLayout = getChildAt(0) as ViewGroup + var widthOfAllTabs = 0 + for (i in 0 until tabLayout.childCount) { + widthOfAllTabs += tabLayout.getChildAt(i).measuredWidth + } + if (widthOfAllTabs <= measuredWidth) { + // fill all space if there is enough room + tabMode = MODE_FIXED + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt new file mode 100644 index 0000000..c335336 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/BackgroundMessageView.kt @@ -0,0 +1,77 @@ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.text.method.LinkMovementMethod +import android.util.AttributeSet +import android.view.Gravity +import android.view.LayoutInflater +import android.view.View +import android.widget.LinearLayout +import android.widget.TextView +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.ViewBackgroundMessageBinding +import com.keylesspalace.tusky.util.addDrawables +import com.keylesspalace.tusky.util.getDrawableRes +import com.keylesspalace.tusky.util.getErrorString +import com.keylesspalace.tusky.util.visible + +/** + * This view is used for screens with content which may be empty or might have failed to download. + */ +class BackgroundMessageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + private val binding = ViewBackgroundMessageBinding.inflate(LayoutInflater.from(context), this) + + init { + gravity = Gravity.CENTER_HORIZONTAL + orientation = VERTICAL + + if (isInEditMode) { + setup(R.drawable.errorphant_offline, R.string.error_network) {} + } + } + + fun setup(throwable: Throwable, listener: ((v: View) -> Unit)? = null) { + setup(throwable.getDrawableRes(), throwable.getErrorString(context), listener) + } + + fun setup( + @DrawableRes imageRes: Int, + @StringRes messageRes: Int, + clickListener: ((v: View) -> Unit)? = null + ) = setup(imageRes, context.getString(messageRes), clickListener) + + /** + * Setup image, message and button. + * If [clickListener] is `null` then the button will be hidden. + */ + fun setup( + @DrawableRes imageRes: Int, + message: String, + clickListener: ((v: View) -> Unit)? = null + ) { + binding.messageTextView.text = message + binding.messageTextView.movementMethod = LinkMovementMethod.getInstance() + binding.imageView.setImageResource(imageRes) + binding.button.setOnClickListener(clickListener) + binding.button.visible(clickListener != null) + binding.helpText.visible(false) + } + + fun showHelp(@StringRes helpRes: Int) { + val size: Int = binding.helpText.textSize.toInt() + 2 + val color = binding.helpText.currentTextColor + val text = context.getText(helpRes) + val textWithDrawables = addDrawables(text, color, size, context) + + binding.helpText.setText(textWithDrawables, TextView.BufferType.SPANNABLE) + + binding.helpText.visible(true) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/BezelImageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/BezelImageView.kt new file mode 100644 index 0000000..f217dde --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/BezelImageView.kt @@ -0,0 +1,48 @@ +/* Copyright 2019 Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.graphics.Outline +import android.util.AttributeSet +import android.view.View +import android.view.ViewOutlineProvider +import com.mikepenz.materialdrawer.view.BezelImageView + +/** + * override BezelImageView from MaterialDrawer library to provide custom outline + */ +class BezelImageView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyle: Int = 0 +) : BezelImageView(context, attrs, defStyle) { + override fun onSizeChanged(w: Int, h: Int, old_w: Int, old_h: Int) { + outlineProvider = CustomOutline(w, h) + } + + private class CustomOutline(var width: Int, var height: Int) : + ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect( + 0, + 0, + width, + height, + if (width < height) width / 8f else height / 8f + ) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/ClickableSpanTextView.kt b/app/src/main/java/com/keylesspalace/tusky/view/ClickableSpanTextView.kt new file mode 100644 index 0000000..1fc01c9 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/ClickableSpanTextView.kt @@ -0,0 +1,408 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.view + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.Paint +import android.graphics.Path +import android.graphics.Rect +import android.graphics.RectF +import android.os.Build +import android.text.Selection +import android.text.Spannable +import android.text.Spanned +import android.text.style.ClickableSpan +import android.text.style.URLSpan +import android.util.AttributeSet +import android.util.Log +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_CANCEL +import android.view.MotionEvent.ACTION_DOWN +import android.view.MotionEvent.ACTION_UP +import android.view.ViewConfiguration +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.view.doOnLayout +import com.keylesspalace.tusky.BuildConfig +import com.keylesspalace.tusky.R +import java.lang.Float.max +import java.lang.Float.min +import kotlin.math.abs + +/** + * Displays text to the user with optional [ClickableSpan]s. Extends the touchable area of the spans + * to ensure they meet the minimum size of 48dp x 48dp for accessibility requirements. + * + * If the touchable area of multiple spans overlap the touch is dispatched to the closest span. + */ +class ClickableSpanTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = android.R.attr.textViewStyle +) : AppCompatTextView(context, attrs, defStyleAttr) { + /** + * Map of [RectF] that enclose the [ClickableSpan] without any additional touchable area. A span + * may extend over more than one line, so multiple entries in this map may point to the same + * span. + */ + private val spanRects = mutableMapOf<RectF, ClickableSpan>() + + /** + * Map of [RectF] that enclose the [ClickableSpan] with the additional touchable area. A span + * may extend over more than one line, so multiple entries in this map may point to the same + * span. + */ + private val delegateRects = mutableMapOf<RectF, ClickableSpan>() + + /** + * The [ClickableSpan] that is used for the point the user has touched. Null if the user is + * not tapping, or the point they have touched is not associated with a span. + */ + private var clickedSpan: ClickableSpan? = null + + /** The minimum size, in pixels, of a touchable area for accessibility purposes */ + private val minDimenPx = resources.getDimensionPixelSize(R.dimen.minimum_touch_target) + + /** + * Debugging helper. Normally false, set this to true to show a border around spans, and + * shade their touchable area. + */ + private val showSpanBoundaries = false + + /** + * Debugging helper. The paint to use to draw a span. + */ + private lateinit var spanDebugPaint: Paint + + /** + * Debugging helper. The paint to use to shade a span's touchable area. + */ + private lateinit var paddingDebugPaint: Paint + + init { + // Initialise debugging paints, if appropriate. Only ever present in debug builds, and + // is optimised out if showSpanBoundaries is false. + if (BuildConfig.DEBUG && showSpanBoundaries) { + spanDebugPaint = Paint() + spanDebugPaint.color = Color.BLACK + spanDebugPaint.style = Paint.Style.STROKE + + paddingDebugPaint = Paint() + paddingDebugPaint.color = Color.MAGENTA + paddingDebugPaint.alpha = 50 + } + } + + override fun onTextChanged( + text: CharSequence?, + start: Int, + lengthBefore: Int, + lengthAfter: Int + ) { + super.onTextChanged(text, start, lengthBefore, lengthAfter) + + // TextView tries to optimise the layout process, and will not perform a layout if the + // new text takes the same area as the old text (see TextView.checkForRelayout()). This + // can result in statuses using the wrong clickable areas as they are never remeasured. + // (https://github.com/tuskyapp/Tusky/issues/3596). Force a layout pass to ensure that + // the spans are measured correctly. + if (!isInLayout) requestLayout() + + doOnLayout { measureSpans() } + } + + /** + * Compute [Rect]s for each [ClickableSpan]. + * + * Each span is associated with at least two Rects. One for the span itself, and one for the + * touchable area around the span. + * + * If the span runs over multiple lines there will be two Rects per line. + */ + private fun measureSpans() { + spanRects.clear() + delegateRects.clear() + + val spannedText = text as? Spanned ?: return + + // The goal is to record all the [Rect]s associated with a span with the same fidelity + // that the user sees when they highlight text in the view to select it. + // + // There's no method in [TextView] or [Layout] that does exactly that. [Layout.getSelection] + // would be perfect, but it's not accessible. However, [Layout.getSelectionPath] is. That + // records the Rects between two characters in the string, and handles text that spans + // multiple lines, is bidirectional, etc. + // + // However, it records them in to a [Path], and a Path has no mechanism to extract the + // Rects saved in to it. + // + // So subclass Path with [RectRecordingPath], which records the data from calls to + // [addRect]. Pass that to `getSelectionPath` to extract all the Rects between start and + // end. + val rects = mutableListOf<RectF>() + val rectRecorder = RectRecordingPath(rects) + + for (span in spannedText.getSpans(0, text.length - 1, ClickableSpan::class.java)) { + rects.clear() + val spanStart = spannedText.getSpanStart(span) + val spanEnd = spannedText.getSpanEnd(span) + + // Collect all the Rects for this span + layout.getSelectionPath(spanStart, spanEnd, rectRecorder) + + // Save them + for (rect in rects) { + // Adjust to account for the view's padding and gravity + rect.offset(totalPaddingLeft.toFloat(), totalPaddingTop.toFloat()) + rect.bottom += extendedPaddingBottom + + // The rect wraps just the span, with no additional touchable area. Save a copy. + spanRects[RectF(rect)] = span + + // Adjust the rect to meet the minimum dimensions + if (rect.height() < minDimenPx) { + val yOffset = (minDimenPx - rect.height()) / 2 + rect.top = max(0f, rect.top - yOffset) + rect.bottom = min(rect.bottom + yOffset, bottom.toFloat()) + } + + if (rect.width() < minDimenPx) { + val xOffset = (minDimenPx - rect.width()) / 2 + rect.left = max(0f, rect.left - xOffset) + rect.right = min(rect.right + xOffset, right.toFloat()) + } + + // Save it + delegateRects[rect] = span + } + } + } + + override fun dispatchTouchEvent(event: MotionEvent): Boolean { + // workaround to https://code.google.com/p/android/issues/detail?id=191430 + // from https://stackoverflow.com/a/36740247 + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O_MR1) { + val startSelection = selectionStart + val endSelection = selectionEnd + + val content = text + if (content is Spannable && (startSelection < 0 || endSelection < 0)) { + Selection.setSelection(content as Spannable?, content.length) + } else if (startSelection != endSelection) { + if (event.actionMasked == ACTION_DOWN) { + text = null + text = content + } + } + } + return super.dispatchTouchEvent(event) + } + + /** + * Handle some touch events. + * + * - [ACTION_DOWN]: Determine which, if any span, has been clicked, and save in clickedSpan + * - [ACTION_UP]: If a span was saved then dispatch the click to that span + * - [ACTION_CANCEL]: Clear the saved span + * + * Defer to the parent class for other touches. + */ + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent?): Boolean { + event ?: return super.onTouchEvent(null) + if (delegateRects.isEmpty()) return super.onTouchEvent(event) + + when (event.action) { + ACTION_DOWN -> { + clickedSpan = null + val x = event.x + val y = event.y + + // If the user has clicked directly on a span then use it, ignoring any overlap + for (entry in spanRects) { + if (!entry.key.contains(x, y)) continue + clickedSpan = entry.value + Log.v(TAG, "span click: ${(clickedSpan as URLSpan).url}") + return super.onTouchEvent(event) + } + + // Otherwise, check to see if it's in a touchable area + var activeEntry: MutableMap.MutableEntry<RectF, ClickableSpan>? = null + + for (entry in delegateRects) { + if (entry == activeEntry) continue + if (!entry.key.contains(x, y)) continue + + if (activeEntry == null) { + activeEntry = entry + continue + } + Log.v( + TAG, + "Overlap: ${(entry.value as URLSpan).url} ${(activeEntry.value as URLSpan).url}" + ) + if (isClickOnFirst(entry.key, activeEntry.key, x, y)) { + activeEntry = entry + } + } + clickedSpan = activeEntry?.value + clickedSpan?.let { Log.v(TAG, "padding click: ${(clickedSpan as URLSpan).url}") } + return super.onTouchEvent(event) + } + ACTION_UP -> { + clickedSpan?.let { + clickedSpan = null + val duration = event.eventTime - event.downTime + if (duration <= ViewConfiguration.getLongPressTimeout()) { + it.onClick(this) + return true + } + } + return super.onTouchEvent(event) + } + ACTION_CANCEL -> { + clickedSpan = null + return super.onTouchEvent(event) + } + else -> return super.onTouchEvent(event) + } + } + + /** + * Determine whether a click on overlapping rectangles should be attributed to the first or the + * second rectangle. + * + * When the user clicks on the overlap it has to be attributed to the "best" rectangle. The + * rectangles have equivalent z-order, so their "closeness" to the user in the Z-plane is not + * a consideration. + * + * The chosen rectangle depends on whether they overlap top/bottom (the top of one rect is + * not the same as the top of the other rect), or they overlap left/right (the tops of both + * rects are the same). + * + * In this example the rectangles overlap top/bottom because their top edges are not aligned. + * + * ``` + * +--------------+ + * |1 | + * | +--------------+ + * | |2 | + * | | | + * | | | + * +------| | + * | | + * +--------------+ + * ``` + * + * (Rectangle #1 being partially occluded by rectangle #2 is for clarity in the diagram, it + * does not affect the algorithm) + * + * Take the Y coordinate of the centre of each rectangle. + * + * ``` + * +--------------+ + * |1 | + * | +--------------+ + * |......|2 | <-- Rect #1 centre line + * | | | + * | |..............| <-- Rect #2 centre line + * +------| | + * | | + * +--------------+ + * ``` + * + * Take the Y position of the click, and determine which Y centre coordinate it is closest too. + * Whichever one is closest is the clicked rectangle. + * + * In these examples the left column of numbers is the Y coordinate, `*` marks the point where + * the user clicked. + * + * ``` + * 0 +--------------+ +--------------+ + * 1 |1 | |1 | + * 2 | +--------------+ | +--------------+ + * 3 |......|2 * | |......|2 | + * 4 | | | | | | + * 5 | |..............| | |*.............| + * 6 +------| | +------| | + * 7 | | | | + * 8 +--------------+ +--------------+ + * + * Rect #1 centre Y = 3 + * Rect #2 centre Y = 5 + * Click (*) Y = 3 Click (*) Y = 5 + * Result: Rect #1 is clicked Result: Rect #2 is clicked + * ``` + * + * The approach is the same if the rectangles overlap left/right, but the X coordinate of the + * centre of the rectangle is tested against the X coordinate of the click. + * + * @param first rectangle to test against + * @param second rectangle to test against + * @param x coordinate of user click + * @param y coordinate of user click + * @return true if the click was closer to the first rectangle than the second + */ + private fun isClickOnFirst(first: RectF, second: RectF, x: Float, y: Float): Boolean { + Log.v(TAG, "first: $first second: $second click: $x $y") + val (firstDiff, secondDiff) = if (first.top == second.top) { + Log.v(TAG, "left/right overlap") + Pair(abs(first.centerX() - x), abs(second.centerX() - x)) + } else { + Log.v(TAG, "top/bottom overlap") + Pair(abs(first.centerY() - y), abs(second.centerY() - y)) + } + Log.d(TAG, "firstDiff: $firstDiff secondDiff: $secondDiff") + return firstDiff < secondDiff + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + // Paint span boundaries. Optimised out on release builds, or debug builds where + // showSpanBoundaries is false. + if (BuildConfig.DEBUG && showSpanBoundaries) { + canvas.save() + for (entry in delegateRects) { + canvas.drawRect(entry.key, paddingDebugPaint) + } + + for (entry in spanRects) { + canvas.drawRect(entry.key, spanDebugPaint) + } + canvas.restore() + } + } + + companion object { + const val TAG = "ClickableSpanTextView" + } +} + +/** + * A [Path] that records the contents of all the [addRect] calls it receives. + * + * @param rects list to record the received [RectF] + */ +private class RectRecordingPath(private val rects: MutableList<RectF>) : Path() { + override fun addRect(left: Float, top: Float, right: Float, bottom: Float, dir: Direction) { + rects.add(RectF(left, top, right, bottom)) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt b/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt new file mode 100644 index 0000000..0a9bfbf --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/EmojiPicker.kt @@ -0,0 +1,17 @@ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.util.AttributeSet +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView + +class EmojiPicker @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : RecyclerView(context, attrs) { + + init { + clipToPadding = false + layoutManager = GridLayoutManager(context, 3, GridLayoutManager.HORIZONTAL, false) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/EndlessOnScrollListener.kt b/app/src/main/java/com/keylesspalace/tusky/view/EndlessOnScrollListener.kt new file mode 100644 index 0000000..c240adf --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/EndlessOnScrollListener.kt @@ -0,0 +1,48 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ +package com.keylesspalace.tusky.view + +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView + +abstract class EndlessOnScrollListener(private val layoutManager: LinearLayoutManager) : + RecyclerView.OnScrollListener() { + private var previousTotalItemCount = 0 + + override fun onScrolled(view: RecyclerView, dx: Int, dy: Int) { + val totalItemCount = layoutManager.itemCount + val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() + + if (totalItemCount < previousTotalItemCount) { + previousTotalItemCount = totalItemCount + } + if (totalItemCount != previousTotalItemCount) { + previousTotalItemCount = totalItemCount + } + if (lastVisibleItemPosition + VISIBLE_THRESHOLD > totalItemCount) { + onLoadMore(totalItemCount, view) + } + } + + fun reset() { + previousTotalItemCount = 0 + } + + abstract fun onLoadMore(totalItemsCount: Int, view: RecyclerView) + + companion object { + private const val VISIBLE_THRESHOLD = 15 + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/GraphView.kt b/app/src/main/java/com/keylesspalace/tusky/view/GraphView.kt new file mode 100644 index 0000000..0d70f01 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/GraphView.kt @@ -0,0 +1,326 @@ +/* Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.view + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.Path +import android.graphics.PathMeasure +import android.graphics.Rect +import android.util.AttributeSet +import android.view.View +import androidx.annotation.ColorInt +import androidx.annotation.Dimension +import androidx.core.content.res.use +import com.google.android.material.R as materialR +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.R +import kotlin.math.max + +class GraphView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + @get:ColorInt + @ColorInt + var primaryLineColor = 0 + + @get:ColorInt + @ColorInt + var secondaryLineColor = 0 + + @get:Dimension + var lineWidth = 0f + + @get:ColorInt + @ColorInt + var graphColor = 0 + + @get:ColorInt + @ColorInt + var metaColor = 0 + + private var proportionalTrending = false + + private lateinit var primaryLinePaint: Paint + private lateinit var secondaryLinePaint: Paint + private lateinit var primaryCirclePaint: Paint + private lateinit var secondaryCirclePaint: Paint + private lateinit var graphPaint: Paint + private lateinit var metaPaint: Paint + + private lateinit var sizeRect: Rect + private var primaryLinePath: Path = Path() + private var secondaryLinePath: Path = Path() + + var maxTrendingValue: Long = 300 + var primaryLineData: List<Long> = if (isInEditMode) { + listOf( + 30, + 60, + 70, + 80, + 130, + 190, + 80 + ) + } else { + listOf( + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ) + } + set(value) { + field = value.map { max(1, it) } + primaryLinePath.reset() + invalidate() + } + + var secondaryLineData: List<Long> = if (isInEditMode) { + listOf( + 10, + 20, + 40, + 60, + 100, + 132, + 20 + ) + } else { + listOf( + 1, + 1, + 1, + 1, + 1, + 1, + 1 + ) + } + set(value) { + field = value.map { max(1, it) } + secondaryLinePath.reset() + invalidate() + } + + init { + initFromXML(attrs) + } + + private fun initFromXML(attr: AttributeSet?) { + context.obtainStyledAttributes(attr, R.styleable.GraphView).use { a -> + primaryLineColor = a.getColor( + R.styleable.GraphView_primaryLineColor, + MaterialColors.getColor(this, materialR.attr.colorPrimary) + ) + + secondaryLineColor = a.getColor( + R.styleable.GraphView_secondaryLineColor, + context.getColor(R.color.warning_color) + ) + + lineWidth = a.getDimension( + R.styleable.GraphView_lineWidth, + context.resources.getDimension(R.dimen.graph_line_thickness) + ) + + graphColor = a.getColor( + R.styleable.GraphView_graphColor, + context.getColor(R.color.colorBackground) + ) + + metaColor = a.getColor( + R.styleable.GraphView_metaColor, + context.getColor(R.color.dividerColor) + ) + + proportionalTrending = a.getBoolean( + R.styleable.GraphView_proportionalTrending, + proportionalTrending + ) + } + + primaryLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = primaryLineColor + strokeWidth = lineWidth + style = Paint.Style.STROKE + } + + primaryCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = primaryLineColor + style = Paint.Style.FILL + } + + secondaryLinePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = secondaryLineColor + strokeWidth = lineWidth + style = Paint.Style.STROKE + } + + secondaryCirclePaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = secondaryLineColor + style = Paint.Style.FILL + } + + graphPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = graphColor + } + + metaPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + color = metaColor + strokeWidth = 0f + style = Paint.Style.STROKE + } + } + + private fun initializeVertices() { + sizeRect = Rect(0, 0, width, height) + + initLine(primaryLineData, primaryLinePath) + initLine(secondaryLineData, secondaryLinePath) + } + + private fun initLine(lineData: List<Long>, path: Path) { + val max = if (proportionalTrending) { + maxTrendingValue + } else { + max(primaryLineData.max(), 1) + } + val mainRatio = height.toFloat() / max.toFloat() + + val ratioedData = lineData.map { it.toFloat() * mainRatio } + + val pointDistance = dataSpacing(ratioedData) + + /** X coord of the start of this path segment */ + var startX = 0F + + /** Y coord of the start of this path segment */ + var startY = 0F + + /** X coord of the end of this path segment */ + var endX: Float + + /** Y coord of the end of this path segment */ + var endY: Float + + /** X coord of bezier control point #1 */ + var controlX1: Float + + /** X coord of bezier control point #2 */ + var controlX2: Float + + // Draw cubic bezier curves between each pair of points. + ratioedData.forEachIndexed { index, magnitude -> + val x = pointDistance * index.toFloat() + val y = height.toFloat() - magnitude + + if (index == 0) { + path.reset() + path.moveTo(x, y) + startX = x + startY = y + } else { + endX = x + endY = y + + // X-coord for a control point is placed one third of the distance between the + // two points. + val offsetX = (endX - startX) / 3 + controlX1 = startX + offsetX + controlX2 = endX - offsetX + path.cubicTo(controlX1, startY, controlX2, endY, x, y) + + startX = x + startY = y + } + } + } + + private fun dataSpacing(data: List<Any>) = width.toFloat() / max(data.size - 1, 1).toFloat() + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + if (primaryLinePath.isEmpty && width > 0) { + initializeVertices() + } + + canvas.apply { + drawRect(sizeRect, graphPaint) + + val pointDistance = dataSpacing(primaryLineData) + + // Vertical tick marks + for (i in 0 until primaryLineData.size + 1) { + drawLine( + i * pointDistance, + height.toFloat(), + i * pointDistance, + height - (height.toFloat() / 20), + metaPaint + ) + } + + // X-axis + drawLine(0f, height.toFloat(), width.toFloat(), height.toFloat(), metaPaint) + + // Data lines + drawLine( + canvas = canvas, + linePath = secondaryLinePath, + linePaint = secondaryLinePaint, + circlePaint = secondaryCirclePaint, + lineThickness = lineWidth + ) + drawLine( + canvas = canvas, + linePath = primaryLinePath, + linePaint = primaryLinePaint, + circlePaint = primaryCirclePaint, + lineThickness = lineWidth + ) + } + } + + private fun drawLine( + canvas: Canvas, + linePath: Path, + linePaint: Paint, + circlePaint: Paint, + lineThickness: Float + ) { + canvas.apply { + drawPath( + linePath, + linePaint + ) + + val pm = PathMeasure(linePath, false) + val coord = floatArrayOf(0f, 0f) + pm.getPosTan(pm.length * 1f, coord, null) + + drawCircle(coord[0], coord[1], lineThickness * 2f, circlePaint) + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt b/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt new file mode 100644 index 0000000..04d689e --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/LicenseCard.kt @@ -0,0 +1,71 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.view + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.LayoutInflater +import androidx.core.content.res.use +import com.google.android.material.R as materialR +import com.google.android.material.card.MaterialCardView +import com.google.android.material.color.MaterialColors +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.CardLicenseBinding +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.openLink + +class LicenseCard +@JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MaterialCardView(context, attrs, defStyleAttr) { + + init { + val binding = CardLicenseBinding.inflate(LayoutInflater.from(context), this) + + setCardBackgroundColor( + MaterialColors.getColor( + context, + materialR.attr.colorSurface, + Color.BLACK + ) + ) + + val (name, license, link) = context.theme.obtainStyledAttributes( + attrs, + R.styleable.LicenseCard, + 0, + 0 + ).use { a -> + Triple( + a.getString(R.styleable.LicenseCard_name), + a.getString(R.styleable.LicenseCard_license), + a.getString(R.styleable.LicenseCard_link) + ) + } + + binding.licenseCardName.text = name + binding.licenseCardLicense.text = license + if (link.isNullOrBlank()) { + binding.licenseCardLink.hide() + } else { + binding.licenseCardLink.text = link + setOnClickListener { context.openLink(link) } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewImageView.kt b/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewImageView.kt new file mode 100644 index 0000000..b68f4b7 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewImageView.kt @@ -0,0 +1,143 @@ +/* Copyright 2018 Jochem Raat <jchmrt@riseup.net> + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.graphics.Matrix +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatImageView +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.util.FocalPointUtil + +/** + * This is an extension of the standard android ImageView, which makes sure to update the custom + * matrix when its size changes if a focal point is set. + * + * If a focal point is set on this view, it will use the FocalPointUtil to update the image + * matrix each time the size of the view is changed. This is needed to ensure that the correct + * cropping is maintained. + * + * However if there is no focal point set (e.g. it is null), then this view should simply + * act exactly the same as an ordinary android ImageView. + */ +open class MediaPreviewImageView +@JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : AppCompatImageView(context, attrs, defStyleAttr), RequestListener<Drawable> { + private var focus: Attachment.Focus? = null + private var focalMatrix: Matrix? = null + + /** + * Set the focal point for this view. + */ + fun setFocalPoint(focus: Attachment.Focus?) { + this.focus = focus + super.setScaleType(ScaleType.MATRIX) + + if (focalMatrix == null) { + focalMatrix = Matrix() + } + } + + /** + * Remove the focal point from this view (if there was one). + */ + fun removeFocalPoint() { + super.setScaleType(ScaleType.CENTER_CROP) + focus = null + } + + /** + * Overridden getScaleType method which returns CENTER_CROP if we have a focal point set. + * + * This is necessary because the Android transitions framework tries to copy the scale type + * from this view to the PhotoView when animating between this view and the detailed view of + * the image. Since the PhotoView does not support a MATRIX scale type, the app would crash + * if we simply passed that on, so instead we pretend that CENTER_CROP is still used here + * even if we have a focus point set. + */ + override fun getScaleType(): ScaleType { + return if (focus != null) { + ScaleType.CENTER_CROP + } else { + super.getScaleType() + } + } + + /** + * Overridden setScaleType method which only accepts the new type if we don't have a focal + * point set. + * + */ + override fun setScaleType(type: ScaleType) { + if (focus != null) { + super.setScaleType(ScaleType.MATRIX) + } else { + super.setScaleType(type) + } + } + + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target<Drawable>, + isFirstResource: Boolean + ): Boolean { + return false + } + + override fun onResourceReady( + resource: Drawable, + model: Any, + target: Target<Drawable>?, + dataSource: DataSource, + isFirstResource: Boolean + ): Boolean { + recalculateMatrix(width, height, resource) + return false + } + + /** + * Called when the size of the view changes, it calls the FocalPointUtil to update the + * matrix if we have a set focal point. It then reassigns the matrix to this imageView. + */ + override fun onSizeChanged(width: Int, height: Int, oldWidth: Int, oldHeight: Int) { + recalculateMatrix(width, height, drawable) + + super.onSizeChanged(width, height, oldWidth, oldHeight) + } + + private fun recalculateMatrix(width: Int, height: Int, drawable: Drawable?) { + if (drawable != null && focus != null && focalMatrix != null) { + scaleType = ScaleType.MATRIX + FocalPointUtil.updateFocalPointMatrix( + width.toFloat(), + height.toFloat(), + drawable.intrinsicWidth.toFloat(), + drawable.intrinsicHeight.toFloat(), + focus as Attachment.Focus, + focalMatrix as Matrix + ) + imageMatrix = focalMatrix + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewLayout.kt b/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewLayout.kt new file mode 100644 index 0000000..1d26d5b --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/MediaPreviewLayout.kt @@ -0,0 +1,214 @@ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.LinearLayout +import android.widget.TextView +import com.keylesspalace.tusky.R +import kotlin.math.roundToInt + +/** + * Lays out a set of [MediaPreviewImageView]s keeping their aspect ratios into account. + */ +class MediaPreviewLayout(context: Context, attrs: AttributeSet? = null) : + ViewGroup(context, attrs) { + + private val spacing = context.resources.getDimensionPixelOffset(R.dimen.preview_image_spacing) + + /** + * An ordered list of aspect ratios used for layout. An image view for each aspect ratio passed + * will be attached. Supports up to 4, additional ones will be ignored. + */ + var aspectRatios: List<Double> = emptyList() + set(value) { + field = value + attachImageViews() + } + + private val imageViewCache = Array(4) { + LayoutInflater.from(context).inflate(R.layout.item_image_preview_overlay, this, false) + } + + private var measuredOrientation = LinearLayout.VERTICAL + + private fun attachImageViews() { + removeAllViews() + for (i in 0 until aspectRatios.size.coerceAtMost(imageViewCache.size)) { + addView(imageViewCache[i]) + } + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val width = MeasureSpec.getSize(widthMeasureSpec) + val halfWidth = width / 2 - spacing / 2 + var totalHeight = 0 + + when (childCount) { + 1 -> { + val aspect = aspectRatios[0] + totalHeight += getChildAt(0).measureToAspect(width, aspect) + } + 2 -> { + val aspect1 = aspectRatios[0] + val aspect2 = aspectRatios[1] + + if ((aspect1 + aspect2) / 2 > 1.2) { + // stack vertically + measuredOrientation = LinearLayout.VERTICAL + totalHeight += getChildAt(0).measureToAspect(width, aspect1.coerceAtLeast(1.8)) + totalHeight += spacing + totalHeight += getChildAt(1).measureToAspect(width, aspect2.coerceAtLeast(1.8)) + } else { + // stack horizontally + measuredOrientation = LinearLayout.HORIZONTAL + val height = rowHeight(halfWidth, aspect1, aspect2) + totalHeight += height + getChildAt(0).measureExactly(halfWidth, height) + getChildAt(1).measureExactly(halfWidth, height) + } + } + 3 -> { + val aspect1 = aspectRatios[0] + val aspect2 = aspectRatios[1] + val aspect3 = aspectRatios[2] + if (aspect1 >= 1) { + // | 1 | + // ------------- + // | 2 | 3 | + measuredOrientation = LinearLayout.VERTICAL + totalHeight += getChildAt(0).measureToAspect(width, aspect1.coerceAtLeast(1.8)) + totalHeight += spacing + val bottomHeight = rowHeight(halfWidth, aspect2, aspect3) + totalHeight += bottomHeight + getChildAt(1).measureExactly(halfWidth, bottomHeight) + getChildAt(2).measureExactly(halfWidth, bottomHeight) + } else { + // | | 2 | + // | 1 |-----| + // | | 3 | + measuredOrientation = LinearLayout.HORIZONTAL + val colHeight = getChildAt(0).measureToAspect(halfWidth, aspect1) + totalHeight += colHeight + val halfHeight = colHeight / 2 - spacing / 2 + getChildAt(1).measureExactly(halfWidth, halfHeight) + getChildAt(2).measureExactly(halfWidth, halfHeight) + } + } + 4 -> { + val aspect1 = aspectRatios[0] + val aspect2 = aspectRatios[1] + val aspect3 = aspectRatios[2] + val aspect4 = aspectRatios[3] + val topHeight = rowHeight(halfWidth, aspect1, aspect2) + totalHeight += topHeight + getChildAt(0).measureExactly(halfWidth, topHeight) + getChildAt(1).measureExactly(halfWidth, topHeight) + totalHeight += spacing + val bottomHeight = rowHeight(halfWidth, aspect3, aspect4) + totalHeight += bottomHeight + getChildAt(2).measureExactly(halfWidth, bottomHeight) + getChildAt(3).measureExactly(halfWidth, bottomHeight) + } + } + + super.onMeasure( + widthMeasureSpec, + MeasureSpec.makeMeasureSpec(totalHeight, MeasureSpec.EXACTLY) + ) + } + + override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) { + val width = r - l + val height = b - t + val halfWidth = width / 2 - spacing / 2 + when (childCount) { + 1 -> { + getChildAt(0).layout(0, 0, width, height) + } + 2 -> { + if (measuredOrientation == LinearLayout.VERTICAL) { + val y = imageViewCache[0].measuredHeight + getChildAt(0).layout(0, 0, width, y) + getChildAt(1).layout( + 0, + y + spacing, + width, + y + spacing + getChildAt(1).measuredHeight + ) + } else { + getChildAt(0).layout(0, 0, halfWidth, height) + getChildAt(1).layout(halfWidth + spacing, 0, width, height) + } + } + 3 -> { + if (measuredOrientation == LinearLayout.VERTICAL) { + val y = getChildAt(0).measuredHeight + getChildAt(0).layout(0, 0, width, y) + getChildAt(1).layout(0, y + spacing, halfWidth, height) + getChildAt(2).layout(halfWidth + spacing, y + spacing, width, height) + } else { + val colHeight = getChildAt(0).measuredHeight + getChildAt(0).layout(0, 0, halfWidth, colHeight) + val halfHeight = colHeight / 2 - spacing / 2 + getChildAt(1).layout(halfWidth + spacing, 0, width, halfHeight) + getChildAt(2).layout( + halfWidth + spacing, + halfHeight + spacing, + width, + colHeight + ) + } + } + 4 -> { + val topHeight = (getChildAt(0).measuredHeight + getChildAt(1).measuredHeight) / 2 + getChildAt(0).layout(0, 0, halfWidth, topHeight) + getChildAt(1).layout(halfWidth + spacing, 0, width, topHeight) + val bottomHeight = + (imageViewCache[2].measuredHeight + imageViewCache[3].measuredHeight) / 2 + getChildAt(2).layout( + 0, + topHeight + spacing, + halfWidth, + topHeight + spacing + bottomHeight + ) + getChildAt(3).layout( + halfWidth + spacing, + topHeight + spacing, + width, + topHeight + spacing + bottomHeight + ) + } + } + } + + inline fun forEachIndexed(action: (Int, MediaPreviewImageView, TextView) -> Unit) { + for (index in 0 until childCount) { + val wrapper = getChildAt(index) + action( + index, + wrapper.findViewById(R.id.preview_image_view), + wrapper.findViewById(R.id.preview_media_description_indicator) + ) + } + } +} + +private fun rowHeight(halfWidth: Int, aspect1: Double, aspect2: Double): Int { + return ((halfWidth / aspect1 + halfWidth / aspect2) / 2).roundToInt() +} + +private fun View.measureToAspect(width: Int, aspect: Double): Int { + val height = (width / aspect).roundToInt() + measureExactly(width, height) + return height +} + +private fun View.measureExactly(width: Int, height: Int) { + measure( + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), + View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY) + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt new file mode 100644 index 0000000..715fa60 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/MuteAccountDialog.kt @@ -0,0 +1,36 @@ +@file:JvmName("MuteAccountDialog") + +package com.keylesspalace.tusky.view + +import android.app.Activity +import androidx.appcompat.app.AlertDialog +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.DialogMuteAccountBinding + +fun showMuteAccountDialog( + activity: Activity, + accountUsername: String, + onOk: (notifications: Boolean, duration: Int?) -> Unit +) { + val binding = DialogMuteAccountBinding.inflate(activity.layoutInflater) + binding.warning.text = activity.getString(R.string.dialog_mute_warning, accountUsername) + binding.checkbox.isChecked = true + + AlertDialog.Builder(activity) + .setView(binding.root) + .setPositiveButton(android.R.string.ok) { _, _ -> + val durationValues = activity.resources.getIntArray(R.array.mute_duration_values) + + // workaround to make indefinite muting work with Mastodon 3.3.0 + // https://github.com/tuskyapp/Tusky/issues/2107 + val duration = if (binding.duration.selectedItemPosition == 0) { + null + } else { + durationValues[binding.duration.selectedItemPosition] + } + + onOk(binding.checkbox.isChecked, duration) + } + .setNegativeButton(android.R.string.cancel, null) + .show() +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/SliderPreference.kt b/app/src/main/java/com/keylesspalace/tusky/view/SliderPreference.kt new file mode 100644 index 0000000..f8ac674 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/SliderPreference.kt @@ -0,0 +1,193 @@ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.content.res.TypedArray +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import androidx.appcompat.content.res.AppCompatResources +import androidx.preference.Preference +import androidx.preference.PreferenceViewHolder +import com.google.android.material.slider.LabelFormatter.LABEL_GONE +import com.google.android.material.slider.Slider +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.databinding.PrefSliderBinding +import com.keylesspalace.tusky.util.hide +import com.keylesspalace.tusky.util.show +import java.lang.Float.max +import java.lang.Float.min + +/** + * Slider preference + * + * Similar to [androidx.preference.SeekBarPreference], but better because: + * + * - Uses a [Slider] instead of a [android.widget.SeekBar]. Slider supports float values, and step sizes + * other than 1. + * - Displays the currently selected value in the Preference's summary, for consistency + * with platform norms. + * - Icon buttons can be displayed at the start/end of the slider. Pressing them will + * increment/decrement the slider by `stepSize`. + * - User can supply a custom formatter to format the summary value + */ +class SliderPreference @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = androidx.preference.R.attr.preferenceStyle, + defStyleRes: Int = 0 +) : Preference(context, attrs, defStyleAttr, defStyleRes), + Slider.OnChangeListener, + Slider.OnSliderTouchListener { + + /** Backing property for `value` */ + private var _value = 0F + + /** + * @see Slider.getValue + * @see Slider.setValue + */ + var value: Float = DEFAULT_VALUE + get() = _value + set(v) { + val clamped = max(max(v, valueFrom), min(v, valueTo)) + if (clamped == field) return + _value = clamped + persistFloat(v) + notifyChanged() + } + + /** @see Slider.setValueFrom */ + var valueFrom: Float + + /** @see Slider.setValueTo */ + var valueTo: Float + + /** @see Slider.setStepSize */ + var stepSize: Float + + /** + * Format string to be applied to values before setting the summary. For more control set + * [SliderPreference.formatter] + */ + var format: String = DEFAULT_FORMAT + + /** + * Function that will be used to format the summary. The default formatter formats using the + * value of the [SliderPreference.format] property. + */ + var formatter: (Float) -> String = { format.format(it) } + + /** + * Optional icon to show in a button at the start of the slide. If non-null the button is + * shown. Clicking the button decrements the value by one step. + */ + var decrementIcon: Drawable? = null + + /** + * Optional icon to show in a button at the end of the slider. If non-null the button is + * shown. Clicking the button increments the value by one step. + */ + var incrementIcon: Drawable? = null + + /** View binding */ + private lateinit var binding: PrefSliderBinding + + init { + // Using `widgetLayoutResource` here would be incorrect, as that tries to put the entire + // preference layout to the right of the title and summary. + layoutResource = R.layout.pref_slider + + val a = context.obtainStyledAttributes( + attrs, + R.styleable.SliderPreference, + defStyleAttr, + defStyleRes + ) + + value = a.getFloat(R.styleable.SliderPreference_android_value, DEFAULT_VALUE) + valueFrom = a.getFloat(R.styleable.SliderPreference_android_valueFrom, DEFAULT_VALUE_FROM) + valueTo = a.getFloat(R.styleable.SliderPreference_android_valueTo, DEFAULT_VALUE_TO) + stepSize = a.getFloat(R.styleable.SliderPreference_android_stepSize, DEFAULT_STEP_SIZE) + format = a.getString(R.styleable.SliderPreference_format) ?: DEFAULT_FORMAT + + val decrementIconResource = a.getResourceId(R.styleable.SliderPreference_iconStart, -1) + if (decrementIconResource != -1) { + decrementIcon = AppCompatResources.getDrawable(context, decrementIconResource) + } + + val incrementIconResource = a.getResourceId(R.styleable.SliderPreference_iconEnd, -1) + if (incrementIconResource != -1) { + incrementIcon = AppCompatResources.getDrawable(context, incrementIconResource) + } + + a.recycle() + } + + override fun onGetDefaultValue(a: TypedArray, i: Int): Any { + return a.getFloat(i, DEFAULT_VALUE) + } + + override fun onSetInitialValue(defaultValue: Any?) { + value = getPersistedFloat((defaultValue ?: DEFAULT_VALUE) as Float) + } + + override fun onBindViewHolder(holder: PreferenceViewHolder) { + super.onBindViewHolder(holder) + binding = PrefSliderBinding.bind(holder.itemView) + + binding.root.isClickable = false + + binding.slider.clearOnChangeListeners() + binding.slider.clearOnSliderTouchListeners() + binding.slider.addOnChangeListener(this) + binding.slider.addOnSliderTouchListener(this) + binding.slider.value = value // sliderValue + binding.slider.valueTo = valueTo + binding.slider.valueFrom = valueFrom + binding.slider.stepSize = stepSize + + // Disable the label, the value is shown in the preference summary + binding.slider.labelBehavior = LABEL_GONE + binding.slider.isEnabled = isEnabled + + binding.summary.show() + binding.summary.text = formatter(value) + + decrementIcon?.let { icon -> + binding.decrement.icon = icon + binding.decrement.show() + binding.decrement.setOnClickListener { + value -= stepSize + } + } ?: binding.decrement.hide() + + incrementIcon?.let { icon -> + binding.increment.icon = icon + binding.increment.show() + binding.increment.setOnClickListener { + value += stepSize + } + } ?: binding.increment.hide() + } + + override fun onValueChange(slider: Slider, value: Float, fromUser: Boolean) { + if (!fromUser) return + binding.summary.text = formatter(value) + } + + override fun onStartTrackingTouch(slider: Slider) { + // Deliberately empty + } + + override fun onStopTrackingTouch(slider: Slider) { + value = slider.value + } + + companion object { + private const val TAG = "SliderPreference" + private const val DEFAULT_VALUE_FROM = 0F + private const val DEFAULT_VALUE_TO = 1F + private const val DEFAULT_VALUE = 0.5F + private const val DEFAULT_STEP_SIZE = 0.1F + private const val DEFAULT_FORMAT = "%3.1f" + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/view/TuskySwipeRefreshLayout.kt b/app/src/main/java/com/keylesspalace/tusky/view/TuskySwipeRefreshLayout.kt new file mode 100644 index 0000000..801f39c --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/TuskySwipeRefreshLayout.kt @@ -0,0 +1,37 @@ +/* Copyright 2024 Tusky contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.util.AttributeSet +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout +import com.google.android.material.R as materialR +import com.google.android.material.color.MaterialColors + +/** + * SwipeRefreshLayout does not allow theming of the color scheme, + * so we use this class to still have a single point to change its colors. + */ +class TuskySwipeRefreshLayout @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : SwipeRefreshLayout(context, attrs) { + + init { + setColorSchemeColors( + MaterialColors.getColor(this, materialR.attr.colorPrimary) + ) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt new file mode 100644 index 0000000..1881c52 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/AttachmentViewData.kt @@ -0,0 +1,52 @@ +/* Copyright 2022 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.viewdata + +import android.os.Parcelable +import com.keylesspalace.tusky.entity.Attachment +import kotlinx.parcelize.IgnoredOnParcel +import kotlinx.parcelize.Parcelize + +@Parcelize +data class AttachmentViewData( + val attachment: Attachment, + val statusId: String, + val statusUrl: String, + val sensitive: Boolean, + val isRevealed: Boolean +) : Parcelable { + + @IgnoredOnParcel + val id = attachment.id + + companion object { + @JvmStatic + fun list( + status: StatusViewData.Concrete, + alwaysShowSensitiveMedia: Boolean = false + ): List<AttachmentViewData> { + return status.attachments.map { attachment -> + AttachmentViewData( + attachment = attachment, + statusId = status.actionableId, + statusUrl = status.actionable.url!!, + sensitive = status.actionable.sensitive, + isRevealed = alwaysShowSensitiveMedia || !status.actionable.sensitive + ) + } + } + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.kt new file mode 100644 index 0000000..54e58c0 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/NotificationViewData.kt @@ -0,0 +1,48 @@ +/* Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ +package com.keylesspalace.tusky.viewdata + +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.entity.Report +import com.keylesspalace.tusky.entity.TimelineAccount + +sealed class NotificationViewData { + + abstract val id: String + + abstract fun asStatusOrNull(): StatusViewData.Concrete? + abstract fun asPlaceholderOrNull(): Placeholder? + + data class Concrete( + override val id: String, + val type: Notification.Type, + val account: TimelineAccount, + val statusViewData: StatusViewData.Concrete?, + val report: Report? + ) : NotificationViewData() { + override fun asStatusOrNull() = statusViewData + + override fun asPlaceholderOrNull() = null + } + + data class Placeholder( + override val id: String, + val isLoading: Boolean + ) : NotificationViewData() { + override fun asStatusOrNull() = null + + override fun asPlaceholderOrNull() = this + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt new file mode 100644 index 0000000..07d9598 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/PollViewData.kt @@ -0,0 +1,93 @@ +/* Copyright 2019 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.viewdata + +import android.content.Context +import android.text.SpannableStringBuilder +import android.text.Spanned +import androidx.core.text.parseAsHtml +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.PollOption +import java.util.Date +import kotlin.math.roundToInt + +data class PollViewData( + val id: String, + val expiresAt: Date?, + val expired: Boolean, + val multiple: Boolean, + val votesCount: Int, + val votersCount: Int?, + val options: List<PollOptionViewData>, + var voted: Boolean +) + +data class PollOptionViewData( + val title: String, + var votesCount: Int, + var selected: Boolean, + var voted: Boolean +) + +fun calculatePercent(fraction: Int?, totalVoters: Int?, totalVotes: Int): Int { + return if (fraction == null || fraction == 0) { + 0 + } else { + val total = totalVoters ?: totalVotes + (fraction / total.toDouble() * 100).roundToInt() + } +} + +fun buildDescription(title: String, percent: Int, voted: Boolean, context: Context): Spanned { + val builder = + SpannableStringBuilder( + context.getString(R.string.poll_percent_format, percent).parseAsHtml() + ) + if (voted) { + builder.append(" ✓ ") + } else { + builder.append(" ") + } + return builder.append(title) +} + +fun Poll?.toViewData(): PollViewData? { + if (this == null) return null + return PollViewData( + id = id, + expiresAt = expiresAt, + expired = expired, + multiple = multiple, + votesCount = votesCount, + votersCount = votersCount, + options = options.mapIndexed { index, option -> + option.toViewData( + ownVotes.contains(index) + ) + }, + voted = voted + ) +} + +fun PollOption.toViewData(voted: Boolean): PollOptionViewData { + return PollOptionViewData( + title = title, + votesCount = votesCount ?: 0, + selected = false, + voted = voted + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt new file mode 100644 index 0000000..4854679 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/StatusViewData.kt @@ -0,0 +1,136 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ +package com.keylesspalace.tusky.viewdata + +import android.text.Spanned +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.Translation +import com.keylesspalace.tusky.util.parseAsMastodonHtml +import com.keylesspalace.tusky.util.shouldTrimStatus + +sealed interface TranslationViewData { + val data: Translation? + + data class Loaded(override val data: Translation) : TranslationViewData + + data object Loading : TranslationViewData { + override val data: Translation? + get() = null + } +} + +/** + * Created by charlag on 11/07/2017. + * + * Class to represent data required to display either a notification or a placeholder. + * It is either a [StatusViewData.Concrete] or a [StatusViewData.Placeholder]. + */ +sealed class StatusViewData { + abstract val id: String + var filterAction: Filter.Action = Filter.Action.NONE + + data class Concrete( + val status: Status, + val isExpanded: Boolean, + val isShowingContent: Boolean, + /** + * Specifies whether the content of this post is currently limited in visibility to the first + * 500 characters or not. + * + * @return Whether the post is collapsed or fully expanded. + */ + val isCollapsed: Boolean, + val isDetailed: Boolean = false, + val translation: TranslationViewData? = null, + ) : StatusViewData() { + override val id: String + get() = status.id + + val content: Spanned = + (translation?.data?.content ?: actionable.content).parseAsMastodonHtml() + + val attachments: List<Attachment> = + actionable.attachments.translated { translation -> map { it.translated(translation) } } + + val spoilerText: String = + actionable.spoilerText.translated { translation -> translation.spoilerText ?: this } + + val poll = actionable.poll?.translated { translation -> + val translatedOptionsText = translation.poll?.options?.map { option -> + option.title + } ?: return@translated this + val translatedOptions = options.zip(translatedOptionsText) { option, translatedText -> + option.copy(title = translatedText) + } + copy(options = translatedOptions) + } + + /** + * Specifies whether the content of this post is long enough to be automatically + * collapsed or if it should show all content regardless. + * Translated posts only show the button if the original post had it as well. + * + * @return Whether the post is collapsible or never collapsed. + */ + val isCollapsible: Boolean = shouldTrimStatus(this.content) && + (translation == null || shouldTrimStatus(actionable.content.parseAsMastodonHtml())) + + val actionable: Status + get() = status.actionableStatus + + val actionableId: String + get() = status.actionableStatus.id + + val rebloggedAvatar: String? + get() = if (status.reblog != null) { + status.account.avatar + } else { + null + } + + val rebloggingStatus: Status? + get() = if (status.reblog != null) status else null + + /** Helper for Java */ + fun copyWithCollapsed(isCollapsed: Boolean): Concrete { + return copy(isCollapsed = isCollapsed) + } + + private fun Attachment.translated(translation: Translation): Attachment { + val translatedDescription = + translation.mediaAttachments.find { it.id == id }?.description + ?: return this + return copy(description = translatedDescription) + } + + private inline fun <T> T.translated(mapper: T.(Translation) -> T): T = + if (translation is TranslationViewData.Loaded) { + mapper(translation.data) + } else { + this + } + } + + data class Placeholder( + override val id: String, + val isLoading: Boolean + ) : StatusViewData() + + fun asStatusOrNull() = this as? Concrete + + fun asPlaceholderOrNull() = this as? Placeholder +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewdata/TrendingViewData.kt b/app/src/main/java/com/keylesspalace/tusky/viewdata/TrendingViewData.kt new file mode 100644 index 0000000..99b0940 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewdata/TrendingViewData.kt @@ -0,0 +1,38 @@ +/* Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.viewdata + +import java.util.Date + +sealed interface TrendingViewData { + val id: String + + data class Header( + val start: Date, + val end: Date + ) : TrendingViewData { + override val id: String = start.toString() + end.toString() + } + + data class Tag( + val name: String, + val usage: List<Long>, + val accounts: List<Long>, + val maxTrendingValue: Long + ) : TrendingViewData { + override val id: String = name + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt new file mode 100644 index 0000000..4f00021 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/AccountsInListViewModel.kt @@ -0,0 +1,129 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.viewmodel + +import android.util.Log +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.NetworkResult +import at.connyduck.calladapter.networkresult.fold +import at.connyduck.calladapter.networkresult.getOrDefault +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.withoutFirstWhich +import dagger.hilt.android.lifecycle.HiltViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch + +data class State( + val accounts: Result<List<TimelineAccount>>, + val searchResult: List<TimelineAccount>? +) + +@HiltViewModel +class AccountsInListViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() { + + val state: Flow<State> get() = _state + private val _state = MutableStateFlow( + State( + accounts = Result.success(emptyList()), + searchResult = null + ) + ) + + fun load(listId: String) { + val state = _state.value + if (state.accounts.isFailure || state.accounts.getOrThrow().isEmpty()) { + viewModelScope.launch { + val accounts = api.getAccountsInList(listId, 0) + _state.update { it.copy(accounts = accounts.toResult()) } + } + } + } + + fun addAccountToList(listId: String, account: TimelineAccount) { + viewModelScope.launch { + api.addAccountToList(listId, listOf(account.id)) + .fold( + onSuccess = { + _state.update { state -> + state.copy(accounts = state.accounts.map { it + account }) + } + }, + onFailure = { + Log.i( + AccountsInListViewModel::class.java.simpleName, + "Failed to add account to list: ${account.username}" + ) + } + ) + } + } + + fun deleteAccountFromList(listId: String, accountId: String) { + viewModelScope.launch { + api.deleteAccountFromList(listId, listOf(accountId)) + .fold( + onSuccess = { + _state.update { state -> + state.copy( + accounts = state.accounts.map { accounts -> + accounts.withoutFirstWhich { it.id == accountId } + } + ) + } + }, + onFailure = { + Log.i( + AccountsInListViewModel::class.java.simpleName, + "Failed to remove account from list: $accountId" + ) + } + ) + } + } + + private val currentQuery = MutableStateFlow("") + + fun search(query: String) { + currentQuery.value = query + } + + init { + viewModelScope.launch { + // Use collectLatest to automatically cancel the previous search + currentQuery.collectLatest { query -> + val searchResult = when { + query.isEmpty() -> null + query.isBlank() -> emptyList() + else -> api.searchAccounts(query, null, 10, true) + .getOrDefault(emptyList()) + } + _state.update { it.copy(searchResult = searchResult) } + } + } + } + + private fun <T> NetworkResult<T>.toResult(): Result<T> = fold( + onSuccess = { Result.success(it) }, + onFailure = { Result.failure(it) } + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt new file mode 100644 index 0000000..a61fe48 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/EditProfileViewModel.kt @@ -0,0 +1,277 @@ +/* Copyright 2018 Conny Duck + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.viewmodel + +import android.app.Application +import android.net.Uri +import androidx.core.net.toUri +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.ProfileEditedEvent +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfo +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.StringField +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.Error +import com.keylesspalace.tusky.util.Loading +import com.keylesspalace.tusky.util.Resource +import com.keylesspalace.tusky.util.Success +import com.keylesspalace.tusky.util.getServerErrorMessage +import com.keylesspalace.tusky.util.randomAlphanumericString +import dagger.hilt.android.lifecycle.HiltViewModel +import java.io.File +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.launch +import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MultipartBody +import okhttp3.RequestBody.Companion.asRequestBody +import okhttp3.RequestBody.Companion.toRequestBody + +private const val HEADER_FILE_NAME = "header.png" +private const val AVATAR_FILE_NAME = "avatar.png" + +internal data class ProfileDataInUi( + val displayName: String, + val note: String, + val locked: Boolean, + val fields: List<StringField> +) + +@HiltViewModel +class EditProfileViewModel @Inject constructor( + private val mastodonApi: MastodonApi, + private val eventHub: EventHub, + private val application: Application, + instanceInfoRepo: InstanceInfoRepository +) : ViewModel() { + + private val _profileData = MutableStateFlow(null as Resource<Account>?) + val profileData: StateFlow<Resource<Account>?> = _profileData.asStateFlow() + + private val _avatarData = MutableStateFlow(null as Uri?) + val avatarData: StateFlow<Uri?> = _avatarData.asStateFlow() + + private val _headerData = MutableStateFlow(null as Uri?) + val headerData: StateFlow<Uri?> = _headerData.asStateFlow() + + private val _saveData = MutableStateFlow(null as Resource<Nothing>?) + val saveData: StateFlow<Resource<Nothing>?> = _saveData.asStateFlow() + + val instanceData: Flow<InstanceInfo> = instanceInfoRepo::getUpdatedInstanceInfoOrFallback.asFlow() + .shareIn(viewModelScope, SharingStarted.Eagerly, replay = 1) + + private val _isChanged = MutableStateFlow(false) + val isChanged = _isChanged.asStateFlow() + + private var apiProfileAccount: Account? = null + + fun obtainProfile() = viewModelScope.launch { + if (_profileData.value == null || _profileData.value is Error) { + _profileData.value = Loading() + + mastodonApi.accountVerifyCredentials().fold( + { profile -> + apiProfileAccount = profile + _profileData.value = Success(profile) + }, + { + _profileData.value = Error() + } + ) + } + } + + fun getAvatarUri() = getCacheFileForName(AVATAR_FILE_NAME).toUri() + + fun getHeaderUri() = getCacheFileForName(HEADER_FILE_NAME).toUri() + + fun newAvatarPicked() { + _avatarData.value = getAvatarUri() + } + + fun newHeaderPicked() { + _headerData.value = getHeaderUri() + } + + internal fun dataChanged(newProfileData: ProfileDataInUi) { + _isChanged.value = getProfileDiff(apiProfileAccount, newProfileData).hasChanges() + } + + internal fun save(newProfileData: ProfileDataInUi) { + if (_saveData.value is Loading || _profileData.value !is Success) { + return + } + + _saveData.value = Loading() + + val diff = getProfileDiff(apiProfileAccount, newProfileData) + if (!diff.hasChanges()) { + // if nothing has changed, there is no need to make an api call + _saveData.value = Success() + return + } + + viewModelScope.launch { + var avatarFileBody: MultipartBody.Part? = null + diff.avatarFile?.let { + avatarFileBody = MultipartBody.Part.createFormData( + "avatar", + randomAlphanumericString(12), + it.asRequestBody("image/png".toMediaTypeOrNull()) + ) + } + + var headerFileBody: MultipartBody.Part? = null + diff.headerFile?.let { + headerFileBody = MultipartBody.Part.createFormData( + "header", + randomAlphanumericString(12), + it.asRequestBody("image/png".toMediaTypeOrNull()) + ) + } + + mastodonApi.accountUpdateCredentials( + diff.displayName?.toRequestBody(MultipartBody.FORM), + diff.note?.toRequestBody(MultipartBody.FORM), + diff.locked?.toString()?.toRequestBody(MultipartBody.FORM), + avatarFileBody, + headerFileBody, + diff.field1?.first?.toRequestBody(MultipartBody.FORM), + diff.field1?.second?.toRequestBody(MultipartBody.FORM), + diff.field2?.first?.toRequestBody(MultipartBody.FORM), + diff.field2?.second?.toRequestBody(MultipartBody.FORM), + diff.field3?.first?.toRequestBody(MultipartBody.FORM), + diff.field3?.second?.toRequestBody(MultipartBody.FORM), + diff.field4?.first?.toRequestBody(MultipartBody.FORM), + diff.field4?.second?.toRequestBody(MultipartBody.FORM) + ).fold( + { newAccountData -> + _saveData.value = Success() + eventHub.dispatch(ProfileEditedEvent(newAccountData)) + }, + { throwable -> + _saveData.value = Error(errorMessage = throwable.getServerErrorMessage()) + } + ) + } + } + + // cache activity state for rotation change + internal fun updateProfile(newProfileData: ProfileDataInUi) { + if (_profileData.value is Success) { + val newProfileSource = _profileData.value?.data?.source?.copy( + note = newProfileData.note, + fields = newProfileData.fields + ) + val newProfile = _profileData.value?.data?.copy( + displayName = newProfileData.displayName, + locked = newProfileData.locked, + source = newProfileSource + ) + + _profileData.value = Success(newProfile) + } + } + + private fun getProfileDiff( + oldProfileAccount: Account?, + newProfileData: ProfileDataInUi + ): DiffProfileData { + val displayName = if (oldProfileAccount?.displayName == newProfileData.displayName) { + null + } else { + newProfileData.displayName + } + + val note = if (oldProfileAccount?.source?.note == newProfileData.note) { + null + } else { + newProfileData.note + } + + val locked = if (oldProfileAccount?.locked == newProfileData.locked) { + null + } else { + newProfileData.locked + } + + val avatarFile = if (_avatarData.value != null) { + getCacheFileForName(AVATAR_FILE_NAME) + } else { + null + } + + val headerFile = if (_headerData.value != null) { + getCacheFileForName(HEADER_FILE_NAME) + } else { + null + } + + // when one field changed, all have to be sent or they unchanged ones would get overridden + val allFieldsUnchanged = oldProfileAccount?.source?.fields == newProfileData.fields + val field1 = calculateFieldToUpdate(newProfileData.fields.getOrNull(0), allFieldsUnchanged) + val field2 = calculateFieldToUpdate(newProfileData.fields.getOrNull(1), allFieldsUnchanged) + val field3 = calculateFieldToUpdate(newProfileData.fields.getOrNull(2), allFieldsUnchanged) + val field4 = calculateFieldToUpdate(newProfileData.fields.getOrNull(3), allFieldsUnchanged) + + return DiffProfileData( + displayName, note, locked, field1, field2, field3, field4, headerFile, avatarFile + ) + } + + private fun calculateFieldToUpdate( + newField: StringField?, + fieldsUnchanged: Boolean + ): Pair<String, String>? { + if (fieldsUnchanged || newField == null) { + return null + } + return Pair( + newField.name, + newField.value + ) + } + + private fun getCacheFileForName(filename: String): File { + return File(application.cacheDir, filename) + } + + private data class DiffProfileData( + val displayName: String?, + val note: String?, + val locked: Boolean?, + val field1: Pair<String, String>?, + val field2: Pair<String, String>?, + val field3: Pair<String, String>?, + val field4: Pair<String, String>?, + val headerFile: File?, + val avatarFile: File? + ) { + fun hasChanges() = displayName != null || note != null || locked != null || + avatarFile != null || headerFile != null || field1 != null || field2 != null || + field3 != null || field4 != null + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt new file mode 100644 index 0000000..4e9144d --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/viewmodel/ListsViewModel.kt @@ -0,0 +1,155 @@ +/* Copyright 2017 Andrew Dawson + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.viewmodel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import at.connyduck.calladapter.networkresult.fold +import com.keylesspalace.tusky.entity.MastoList +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.util.replacedFirstWhich +import com.keylesspalace.tusky.util.withoutFirstWhich +import dagger.hilt.android.lifecycle.HiltViewModel +import java.io.IOException +import java.net.ConnectException +import javax.inject.Inject +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.launch + +@HiltViewModel +internal class ListsViewModel @Inject constructor(private val api: MastodonApi) : ViewModel() { + enum class LoadingState { + INITIAL, + LOADING, + LOADED, + ERROR_NETWORK, + ERROR_OTHER + } + + enum class Event { + CREATE_ERROR, + DELETE_ERROR, + UPDATE_ERROR + } + + data class State(val lists: List<MastoList>, val loadingState: LoadingState) + + private val _state = MutableStateFlow(State(listOf(), LoadingState.INITIAL)) + val state: StateFlow<State> = _state.asStateFlow() + + private val _events = MutableSharedFlow<Event>( + replay = 0, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + val events: SharedFlow<Event> = _events.asSharedFlow() + + fun retryLoading() { + loadIfNeeded() + } + + private fun loadIfNeeded() { + val state = _state.value + if (state.loadingState == LoadingState.LOADING || state.lists.isNotEmpty()) return + updateState { + copy(loadingState = LoadingState.LOADING) + } + + viewModelScope.launch { + api.getLists().fold( + { lists -> + updateState { + copy( + lists = lists, + loadingState = LoadingState.LOADED + ) + } + }, + { err -> + updateState { + copy( + loadingState = if (err is IOException || err is ConnectException) { + LoadingState.ERROR_NETWORK + } else { + LoadingState.ERROR_OTHER + } + ) + } + } + ) + } + } + + fun createNewList(listName: String, exclusive: Boolean, replyPolicy: String) { + viewModelScope.launch { + api.createList(listName, exclusive, replyPolicy).fold( + { list -> + updateState { + copy(lists = lists + list) + } + }, + { + sendEvent(Event.CREATE_ERROR) + } + ) + } + } + + fun updateList(listId: String, listName: String, exclusive: Boolean, replyPolicy: String) { + viewModelScope.launch { + api.updateList(listId, listName, exclusive, replyPolicy).fold( + { list -> + updateState { + copy(lists = lists.replacedFirstWhich(list) { it.id == listId }) + } + }, + { + sendEvent(Event.UPDATE_ERROR) + } + ) + } + } + + fun deleteList(listId: String) { + viewModelScope.launch { + api.deleteList(listId).fold( + { + updateState { + copy(lists = lists.withoutFirstWhich { it.id == listId }) + } + }, + { + sendEvent(Event.DELETE_ERROR) + } + ) + } + } + + private inline fun updateState(fn: State.() -> State) { + _state.value = fn(_state.value) + } + + private suspend fun sendEvent(event: Event) { + _events.emit(event) + } +} diff --git a/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt b/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt new file mode 100644 index 0000000..be249bc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/worker/NotificationWorker.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.worker + +import android.app.Notification +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.ForegroundInfo +import androidx.work.WorkerParameters +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.systemnotifications.NotificationFetcher +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper.NOTIFICATION_ID_FETCH_NOTIFICATION +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +/** Fetch and show new notifications. */ +@HiltWorker +class NotificationWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted params: WorkerParameters, + private val notificationsFetcher: NotificationFetcher +) : CoroutineWorker(appContext, params) { + val notification: Notification = NotificationHelper.createWorkerNotification( + applicationContext, + R.string.notification_notification_worker + ) + + override suspend fun doWork(): Result { + notificationsFetcher.fetchAndShow() + return Result.success() + } + + override suspend fun getForegroundInfo() = ForegroundInfo( + NOTIFICATION_ID_FETCH_NOTIFICATION, + notification + ) +} diff --git a/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt b/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt new file mode 100644 index 0000000..b735ddc --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/worker/PruneCacheWorker.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.worker + +import android.app.Notification +import android.content.Context +import android.util.Log +import androidx.hilt.work.HiltWorker +import androidx.work.CoroutineWorker +import androidx.work.ForegroundInfo +import androidx.work.WorkerParameters +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper.NOTIFICATION_ID_PRUNE_CACHE +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.DatabaseCleaner +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject + +/** Prune the database cache of old statuses. */ +@HiltWorker +class PruneCacheWorker @AssistedInject constructor( + @Assisted appContext: Context, + @Assisted workerParams: WorkerParameters, + private val databaseCleaner: DatabaseCleaner, + private val accountManager: AccountManager +) : CoroutineWorker(appContext, workerParams) { + val notification: Notification = NotificationHelper.createWorkerNotification( + applicationContext, + R.string.notification_prune_cache + ) + + override suspend fun doWork(): Result { + for (account in accountManager.accounts) { + Log.d(TAG, "Pruning database using account ID: ${account.id}") + databaseCleaner.cleanupOldData(account.id, MAX_HOMETIMELINE_ITEMS_IN_CACHE, MAX_NOTIFICATIONS_IN_CACHE) + } + return Result.success() + } + + override suspend fun getForegroundInfo() = ForegroundInfo( + NOTIFICATION_ID_PRUNE_CACHE, + notification + ) + + companion object { + private const val TAG = "PruneCacheWorker" + private const val MAX_HOMETIMELINE_ITEMS_IN_CACHE = 1000 + private const val MAX_NOTIFICATIONS_IN_CACHE = 1000 + const val PERIODIC_WORK_TAG = "PruneCacheWorker_periodic" + } +} diff --git a/app/src/main/res/anim/activity_close_enter.xml b/app/src/main/res/anim/activity_close_enter.xml new file mode 100644 index 0000000..2c08910 --- /dev/null +++ b/app/src/main/res/anim/activity_close_enter.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:shareInterpolator="false"> + <!-- + No-op. + + If we had extend we could do a bit of a fancy translate but without it it we will have a + weird gap at the gap of the screen. + + We put no-op alpha translation because otherwise Android will compose the other part of the + transition over the black background. + --> + <alpha + android:fromAlpha="1.0" + android:toAlpha="1.0" + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@android:anim/linear_interpolator" + android:startOffset="0" + android:duration="450" /> +</set> diff --git a/app/src/main/res/anim/activity_close_exit.xml b/app/src/main/res/anim/activity_close_exit.xml new file mode 100644 index 0000000..feb5939 --- /dev/null +++ b/app/src/main/res/anim/activity_close_exit.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:shareInterpolator="false" + android:zAdjustment="top"> + <alpha + android:fromAlpha="1.0" + android:toAlpha="0.0" + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@android:anim/linear_interpolator" + android:startOffset="35" + android:duration="83" /> + + <translate + android:fromXDelta="0" + android:toXDelta="10%" + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@anim/fast_out_extra_slow_in" + android:startOffset="0" + android:duration="450" /> + +</set> diff --git a/app/src/main/res/anim/activity_open_enter.xml b/app/src/main/res/anim/activity_open_enter.xml new file mode 100644 index 0000000..f2bd3cc --- /dev/null +++ b/app/src/main/res/anim/activity_open_enter.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:shareInterpolator="false"> + <alpha + android:fromAlpha="0.0" + android:toAlpha="1.0" + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@android:anim/linear_interpolator" + android:startOffset="50" + android:duration="83" /> + + <translate + android:fromXDelta="10%" + android:toXDelta="0" + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@anim/fast_out_extra_slow_in" + android:duration="450" /> + +</set> diff --git a/app/src/main/res/anim/activity_open_exit.xml b/app/src/main/res/anim/activity_open_exit.xml new file mode 100644 index 0000000..2c08910 --- /dev/null +++ b/app/src/main/res/anim/activity_open_exit.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android" + android:shareInterpolator="false"> + <!-- + No-op. + + If we had extend we could do a bit of a fancy translate but without it it we will have a + weird gap at the gap of the screen. + + We put no-op alpha translation because otherwise Android will compose the other part of the + transition over the black background. + --> + <alpha + android:fromAlpha="1.0" + android:toAlpha="1.0" + android:fillEnabled="true" + android:fillBefore="true" + android:fillAfter="true" + android:interpolator="@android:anim/linear_interpolator" + android:startOffset="0" + android:duration="450" /> +</set> diff --git a/app/src/main/res/anim/explode.xml b/app/src/main/res/anim/explode.xml new file mode 100644 index 0000000..08001ae --- /dev/null +++ b/app/src/main/res/anim/explode.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android" > + <scale + android:duration="300" + android:fromXScale="0" + android:fromYScale="0" + android:pivotX="50%" + android:pivotY="50%" + android:toXScale="1" + android:toYScale="1" > + </scale> +</set> diff --git a/app/src/main/res/anim/fast_out_extra_slow_in.xml b/app/src/main/res/anim/fast_out_extra_slow_in.xml new file mode 100644 index 0000000..f419778 --- /dev/null +++ b/app/src/main/res/anim/fast_out_extra_slow_in.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2017 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License + --> +<pathInterpolator xmlns:android="http://schemas.android.com/apk/res/android" + android:pathData="M 0,0 C 0.05, 0, 0.133333, 0.06, 0.166666, 0.4 C 0.208333, 0.82, 0.25, 1, 1, 1"/> diff --git a/app/src/main/res/color/account_tab_font_color.xml b/app/src/main/res/color/account_tab_font_color.xml new file mode 100644 index 0000000..c81c01a --- /dev/null +++ b/app/src/main/res/color/account_tab_font_color.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="false" android:color="?android:attr/textColorPrimary"/> + <item android:state_pressed="true" android:color="?android:attr/textColorTertiary"/> +</selector> \ No newline at end of file diff --git a/app/src/main/res/color/color_background_transparent_60.xml b/app/src/main/res/color/color_background_transparent_60.xml new file mode 100644 index 0000000..0a09f2a --- /dev/null +++ b/app/src/main/res/color/color_background_transparent_60.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:alpha="0.6" android:color="@color/colorBackground" /> +</selector> \ No newline at end of file diff --git a/app/src/main/res/color/compound_button_color.xml b/app/src/main/res/color/compound_button_color.xml new file mode 100644 index 0000000..f8dbe90 --- /dev/null +++ b/app/src/main/res/color/compound_button_color.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_checked="false" android:color="?android:attr/textColorTertiary"/> + <item android:state_checked="true" android:color="?attr/colorPrimary"/> +</selector> diff --git a/app/src/main/res/color/text_input_layout_box_stroke_color.xml b/app/src/main/res/color/text_input_layout_box_stroke_color.xml new file mode 100644 index 0000000..3f808bd --- /dev/null +++ b/app/src/main/res/color/text_input_layout_box_stroke_color.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_focused="false" android:color="?android:attr/textColorTertiary"/> + <item android:state_focused="true" android:color="?attr/colorPrimary"/> +</selector> diff --git a/app/src/main/res/drawable/avatar_border.xml b/app/src/main/res/drawable/avatar_border.xml new file mode 100644 index 0000000..aca56e1 --- /dev/null +++ b/app/src/main/res/drawable/avatar_border.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <solid android:color="?android:attr/windowBackground" /> + <corners android:radius="7dp"/> + <size android:height="52dp" android:width="52dp"/> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/avatar_default.xml b/app/src/main/res/drawable/avatar_default.xml new file mode 100644 index 0000000..47e0bb0 --- /dev/null +++ b/app/src/main/res/drawable/avatar_default.xml @@ -0,0 +1,36 @@ +<vector android:height="48dp" android:viewportHeight="211.66667" + android:viewportWidth="211.66666" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillAlpha="1" android:fillColor="#d9e1e8" + android:fillType="nonZero" + android:pathData="M25,0L186.667,0A25,25 0,0 1,211.667 25L211.667,186.667A25,25 0,0 1,186.667 211.667L25,211.667A25,25 0,0 1,0 186.667L0,25A25,25 0,0 1,25 0z" + android:strokeAlpha="1" android:strokeColor="#00000000" + android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="0"/> + <path android:fillAlpha="0.58823529" android:fillColor="#9baec8" + android:pathData="m139.619,113.342c11.68,3.648 12.384,9.062 22.905,-0.907 3.562,-4.085 4.005,-9.454 5.288,-12.341 4.894,-11.009 12.569,-15.71 14.896,-27.12 0.473,-16.199 -8.335,-26.588 -23.586,-29.709 -9.33,-0.806 -18.486,1.531 -27.214,11.566z" + android:strokeAlpha="1" android:strokeColor="#00000000" + android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="0.26458332"/> + <path android:fillAlpha="1" android:fillColor="#c1cddb" + android:pathData="m134.413,117.765c2.949,9.48 5.755,17.442 13.38,21.545 -2.306,1.694 -3.931,1.584 -6.237,1.814 -8.544,-1.912 -13.79,-5.54 -19.617,-11.679z" + android:strokeAlpha="1" android:strokeColor="#c1cddb" + android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="3.5"/> + <path android:fillAlpha="0.58823529" android:fillColor="#9baec8" + android:pathData="m72.047,113.342c-11.68,3.648 -12.384,9.062 -22.905,-0.907 -3.562,-4.085 -4.005,-9.454 -5.288,-12.341 -4.894,-11.009 -12.569,-15.71 -14.896,-27.12 -0.473,-16.199 8.335,-26.588 23.586,-29.709 9.33,-0.806 18.486,1.531 27.214,11.566z" + android:strokeAlpha="1" android:strokeColor="#00000000" + android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="0.26458332"/> + <path android:fillAlpha="1" android:fillColor="#c1cddb" + android:pathData="m76.129,117.765c-2.949,9.48 -5.755,17.442 -13.38,21.545 2.306,1.694 3.931,1.584 6.237,1.814 8.544,-1.912 13.79,-5.54 19.617,-11.679z" + android:strokeAlpha="1" android:strokeColor="#c1cddb" + android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="3.5"/> + <path android:fillAlpha="1" android:fillColor="#a3b6cf" + android:pathData="m105.498,44.172c18.166,0.996 32.248,10.811 37.76,31.977 1.938,10.481 1.118,21.686 -1.814,33.111 -3.378,10.226 -10.89,17.88 -19.05,24.493 -1.178,7.925 -1.131,7.769 -3.402,12.587 -7.086,13.578 -15.673,16.75 -22.34,19.91 -6.719,1.966 -12.521,2.096 -18.358,2.298l0.217,-13.364c2.802,-0.375 5.681,-0.362 8.197,-2.165 2.206,-1.549 3.108,-3.358 3.823,-5.205 1.404,-4.981 1.316,-8.966 -0,-12.133 -9.521,-6.272 -17.6,-16.195 -20.978,-26.42 -2.932,-11.425 -3.752,-22.63 -1.814,-33.111 5.511,-21.166 19.594,-30.981 37.76,-31.977z" + android:strokeAlpha="0" android:strokeColor="#00000000" + android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="0.26458332"/> + <path android:fillColor="#00000000" + android:pathData="m77.382,102.118c4.822,-10.083 12.948,-7.83 15.866,0.15" + android:strokeAlpha="1" android:strokeColor="#7c95b6" + android:strokeLineCap="round" android:strokeLineJoin="miter" android:strokeWidth="2.08333325"/> + <path android:fillColor="#00000000" + android:pathData="m134.619,102.118c-4.822,-10.083 -12.948,-7.83 -15.866,0.15" + android:strokeAlpha="1" android:strokeColor="#7c95b6" + android:strokeLineCap="round" android:strokeLineJoin="miter" android:strokeWidth="2.08333325"/> +</vector> diff --git a/app/src/main/res/drawable/background_dialog_activity.xml b/app/src/main/res/drawable/background_dialog_activity.xml new file mode 100644 index 0000000..80cff38 --- /dev/null +++ b/app/src/main/res/drawable/background_dialog_activity.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="8dp" /> + <solid android:color="?android:attr/colorBackground" /> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/bot_badge.xml b/app/src/main/res/drawable/bot_badge.xml new file mode 100644 index 0000000..6f857df --- /dev/null +++ b/app/src/main/res/drawable/bot_badge.xml @@ -0,0 +1,13 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M5.7407,0L18.2593,0A5.7407,5.7407 0,0 1,24 5.7407L24,18.2593A5.7407,5.7407 0,0 1,18.2593 24L5.7407,24A5.7407,5.7407 0,0 1,0 18.2593L0,5.7407A5.7407,5.7407 0,0 1,5.7407 0z" + android:fillAlpha="0.75" + android:fillColor="@color/botBadgeBackground" /> + <path + android:fillColor="@color/botBadgeForeground" + android:pathData="m12,3.1674a1.6059,1.6059 0,0 1,1.6059 1.6059c0,0.5942 -0.3212,1.1161 -0.803,1.3891v1.0198h0.803a5.6207,5.6207 0,0 1,5.6207 5.6207h0.803a0.803,0.803 0,0 1,0.803 0.803v2.4089a0.803,0.803 0,0 1,-0.803 0.803h-0.803v0.803a1.6059,1.6059 0,0 1,-1.6059 1.6059H6.3793A1.6059,1.6059 0,0 1,4.7733 17.6207V16.8178H3.9704A0.803,0.803 0,0 1,3.1674 16.0148V13.6059A0.803,0.803 0,0 1,3.9704 12.803H4.7733a5.6207,5.6207 0,0 1,5.6207 -5.6207h0.803V6.1625C10.7153,5.8894 10.3941,5.3675 10.3941,4.7733A1.6059,1.6059 0,0 1,12 3.1674M8.3867,12A2.0074,2.0074 0,0 0,6.3793 14.0074,2.0074 2.0074,0 0,0 8.3867,16.0148 2.0074,2.0074 0,0 0,10.3941 14.0074,2.0074 2.0074,0 0,0 8.3867,12m7.2267,0a2.0074,2.0074 0,0 0,-2.0074 2.0074,2.0074 2.0074,0 0,0 2.0074,2.0074 2.0074,2.0074 0,0 0,2.0074 -2.0074A2.0074,2.0074 0,0 0,15.6133 12Z" /> +</vector> diff --git a/app/src/main/res/drawable/card_frame.xml b/app/src/main/res/drawable/card_frame.xml new file mode 100644 index 0000000..525731b --- /dev/null +++ b/app/src/main/res/drawable/card_frame.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="6dp" /> + <stroke android:color="?attr/colorBackgroundAccent" android:width="1dp" /> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/card_image_placeholder.xml b/app/src/main/res/drawable/card_image_placeholder.xml new file mode 100644 index 0000000..1ca515a --- /dev/null +++ b/app/src/main/res/drawable/card_image_placeholder.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="48dp" + android:height="48dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="?android:attr/textColorTertiary" + android:pathData="M6,2A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2H6M6,4H13V9H18V20H6V4M8,12V14H16V12H8M8,16V18H13V16H8Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/conversation_thread_line.xml b/app/src/main/res/drawable/conversation_thread_line.xml new file mode 100644 index 0000000..5a87f79 --- /dev/null +++ b/app/src/main/res/drawable/conversation_thread_line.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <size android:width="4dp" /> + <solid android:color="?attr/dividerColor" /> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/description_bg_expanded.xml b/app/src/main/res/drawable/description_bg_expanded.xml new file mode 100644 index 0000000..8a45cb7 --- /dev/null +++ b/app/src/main/res/drawable/description_bg_expanded.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners + android:topLeftRadius="6dp" + android:topRightRadius="6dp" /> + <stroke + android:color="?attr/colorOutline" + android:width="1dp" /> + <solid android:color="?attr/colorSurface" /> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/elephant_friend.xml b/app/src/main/res/drawable/elephant_friend.xml new file mode 100644 index 0000000..e318667 --- /dev/null +++ b/app/src/main/res/drawable/elephant_friend.xml @@ -0,0 +1,78 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="160dp" + android:height="178dp" + android:viewportWidth="520" + android:viewportHeight="580"> + <path + android:pathData="M464.58,257.93c9,-0.87 16.14,-4.1 22.16,-9.94 0.35,-0.35 0.6,-0.8 0.9,-1.17 2.6,-3.1 4.94,-7.18 9.7,-6 8.63,2.17 9.46,19.48 8.04,27.45 -2.27,12.7 -15.7,28.74 -32.62,35.6 -12.25,4.96 -24.58,7.38 -37.55,7.56 -3.2,0.05 -5.85,1.06 -7.62,3.62 -11.13,16.1 -27.1,24.06 -45.85,27.12 -4.9,0.8 -6.02,2.84 -6.35,7.64 -1.16,16.88 4.23,31.18 14.22,44.6 7.1,9.5 12.35,20.34 19.04,30.18 10.18,14.96 17.16,31.45 23.9,48.05 2.45,6 3.75,12.48 5.46,18.75 2.57,9.45 -3.48,19.32 -14.44,25.83 -12.1,7.18 -25.26,10.55 -39.16,10.48 -5.64,-0.02 -9.43,3.12 -13.65,5.57 -3.18,1.85 -6,4.53 -8.57,7.23 -12.92,13.6 -28.9,20.17 -47.43,21.3 -8.83,0.53 -59.78,16.9 -123.24,8.78 -21.94,-2.82 -46.9,-7.1 -65.27,-15.48 -13.75,-6.3 -34.63,-21.73 -39.26,-36 -1.2,-3.7 -1.98,-3.43 -4.3,-2.18 -7.3,3.93 -15.16,4.83 -23.26,4.33 -14.96,-0.9 -26.27,-7.77 -33.72,-20.9 -5.6,-9.83 -9.05,-20.23 -8.48,-31.68 0.1,-2.2 -1.33,-2.7 -2.8,-3.43 -4.3,-2.08 -7.4,-5.33 -9.3,-9.68 -2.25,-5.15 -1.1,-8.83 3.95,-11.24 2.32,-1.1 2.5,-2.5 2.26,-4.65 -0.88,-8.4 2.67,-14.56 9.92,-18.62 6.56,-3.68 12.47,-1.43 14.5,5.84 1.18,4.23 2.5,6.9 7.75,6.54 4.86,-0.33 8.34,2.58 9.86,7.3 1.4,4.3 -1.13,10.6 -5.2,13.4 -1.88,1.28 -3.72,2.73 -5.8,3.6 -4.46,1.84 -4.5,5.17 -3.25,8.88 1.77,5.3 4.23,10.28 7.58,14.76 4.47,6 12.88,6.42 17.92,0.98 4.64,-5 5.95,-10.86 5.1,-17.6 -1.64,-13.3 -1.74,-26.68 0.23,-40 1.1,-7.43 3.4,-14.64 5.9,-21.7 7.34,-20.78 14.77,-41.55 30.63,-57.9 4.12,-4.24 8.4,-8.16 13.42,-11.32 1.3,-0.84 3.36,-1.62 0.95,-3.5 -6.4,-5.02 -7.04,-12.57 -8.6,-19.7 -0.43,-1.9 -0.85,-3.86 -0.8,-5.8 0.05,-3.8 -1,-6.2 -5.08,-7.56 -3.14,-1.04 -3.9,-4.64 -4,-7.85 -0.75,-23.95 4.8,-46.1 17.6,-66.64 10.64,-17.05 21.42,-33.95 33.88,-49.72 2.43,-3.07 5.1,-6.94 8.3,-9.25 2.47,-2.42 4.02,-5.3 3.25,-9.43 -2.45,-13.18 -4.05,-25.76 -2.9,-38.73 1.16,-12.8 12.96,-25.3 28.64,-18.68 2.1,0.88 3.03,-0.28 3.8,-1.85 5.56,-11.32 25.42,-26.14 35.6,-27.08 5.4,-0.5 11.06,3.56 12.63,10.08 1.06,4.38 2.12,6.15 7.37,4.7 5.6,-1.57 11.84,-1.57 15.74,4.57 1.37,2.14 2.5,4.48 1.77,6.92 -2.34,6.77 0.22,6.7 4.22,6.64 19.47,-0.34 36.57,11.34 52.55,22.58 4.43,3.12 7.88,7.36 11.35,11.43 9.5,11.2 18.47,22.87 29.36,32.8 8.34,7.58 17.73,13.18 29.46,13.54 1.8,0.06 3.6,0.16 5.38,0 9.7,-0.88 13.12,-7.9 8.15,-16.43 -3.77,-6.46 -9.08,-12 -11.58,-19.24 -3.07,-8.9 -4.93,-17.73 1.06,-26.2 1.36,-1.94 0.78,-3.72 0.17,-5.6 -1.98,-6.1 -0.9,-12.03 1.47,-17.67 1.57,-3.76 4.62,-6.53 9,-6.6 4.3,-0.04 8.12,1.35 10.13,5.58 1.33,2.8 2.57,5.65 3.64,8.55 1.63,4.44 5.64,6.35 9.57,3.78 2.73,-1.77 3.28,-4.88 5.76,-7 3.8,-3.24 7.8,-4.04 12.02,-1 4.23,3.04 6.86,7 6,12.5 -0.33,2.25 -0.96,4.45 -2.46,6.32 -3.24,4.02 -2.96,7.96 0.14,12.03 2.95,3.9 5.88,7.86 9,11.57C473.3,155 480.12,170 483.53,186.44c2.06,9.93 1.8,20.08 -0.46,30.13 -3.2,14.18 -8.4,27.4 -17.4,38.95 -0.37,0.48 -0.52,1.12 -1.1,2.4z" + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd"/> + <path + android:pathData="M434.05,70.75c-4.33,1.58 -7.75,1.33 -10.1,-1.72 -2.36,-3.08 -2.15,-7.75 0.5,-10.18 2.44,-2.23 6.73,-3.17 6.73,-3.17s-5.97,-18.84 -7.52,-24.82c-0.6,-2.52 -0.92,-5.7 4.3,-8.72 7.68,-4.42 15.2,-7.8 24.1,-10.7 3.28,-1.06 7.05,-0.3 8.7,2.68 4.12,11.28 7,21.06 10.66,32.28 3.1,9.54 -1.33,10.92 -4.82,13 -2.74,1.63 -6.45,2.92 -9.94,0.64 -3,-1.96 -3.2,-6.7 -2.07,-9.67 1.95,-3.74 8.18,-5.2 8.18,-5.2s-2.03,-6.4 -3.1,-9.5c-4.27,-12.07 -4.44,-9.3 -16.1,-4.23 -10.4,4.52 -9.9,5.07 -6.54,15.98 0.76,2.48 2.74,7.93 3.74,11.88 2.02,7.9 -1.32,9.48 -6.73,11.45zM357.86,54.52c-0.06,-6.67 9.1,-15.55 15.24,-15.43 2.4,0.03 4.03,1.26 5.13,3.2 5.96,10.4 11.24,21.16 17.7,31.3 2.23,3.47 1.18,6.97 -1.84,10.06 -3.42,3.48 -6.68,4.82 -11,1.6 -7.96,-5.9 -25.96,-26.6 -25.24,-30.74zM506.52,73.77c0.53,2.86 -2.1,4.22 -6.44,6.7 -5.93,3.4 -14.07,6.7 -20.86,10.55 -3.48,1.98 -7.18,4.66 -10.96,0.57 -3.46,-3.75 -3.74,-11.26 -0.4,-14.8 8.26,-8.74 16.8,-17.2 26.12,-24.83 3.9,-3.2 6.88,-2.52 8.86,2.1 2.4,5.6 3.3,11.57 3.68,19.7zM485.48,119.2c-10.47,0.63 -11.15,0.1 -12.55,-10.14 -0.86,-6.2 0.57,-8.46 6.56,-9.66 9.93,-2 19.34,-5.8 29.12,-8.33 6.37,-1.65 8.36,-0.26 8.5,6.3 0.17,6.6 -2.64,12.2 -6.38,17.08 -3.2,4.18 -24.45,4.7 -25.26,4.75zM405.4,26.1c0.38,7.18 1.68,23.77 1.65,24.9 -0.1,3.47 -0.96,6.85 -4.7,7.7 -3.56,0.83 -5.83,-2.2 -7.2,-4.75 -3.84,-7.16 -6.7,-14.8 -8.8,-22.66 -0.68,-2.53 -1.16,-5.1 -1.98,-7.55 -2.1,-6.2 -1.5,-9.24 3.5,-13.36 2.86,-2.38 5.83,-4.8 9.43,-6 4.92,-1.64 7.73,0.03 8.03,5.1 0.33,5.52 0.08,11.07 0.08,16.6z" + android:fillColor="@color/elephant_friend_accent_color" + android:fillType="evenOdd"/> + <path + android:pathData="M237.55,461.68c-3.74,-4.52 -5.68,-10.24 -9.82,-14.5 -8.22,-8.45 -18.98,-11.4 -29.97,-12.43 -13.3,-1.25 -25.84,2.5 -37.3,9.43 -1.23,0.75 -4.1,1 -2.96,3.46 1.12,2.44 3.37,1.07 5.2,0.6 0.63,-0.17 1.3,-0.3 1.84,-0.64 12.76,-7.84 26.86,-6.72 39.83,-2.92 12.03,3.52 22.96,10.7 28.42,23.56 7.16,16.86 10.88,34.32 10.7,52.63 0,2.65 -0.3,4.42 -3.3,5.6 -7.93,3.1 -10.82,10.33 -7.84,18.34 0.33,0.87 0.67,1.73 1,2.6 -1.97,0.64 -2.25,-0.7 -2.8,-1.58 -3.06,-4.83 -7.6,-6.5 -13.04,-5.87 -5.22,0.6 -7.82,4.27 -9.36,8.86 -1.05,3.13 -1.84,6.34 -1.14,9.63 0.37,1.73 0.28,2.84 -1.7,3.23 -1.8,0.35 -2.7,-0.2 -3.1,-2.07 -0.83,-3.67 -3.48,-6 -6.05,-8.6 -7.77,-7.88 -17.46,-2.88 -19.98,6.12 -0.96,3.38 -2.3,3.72 -5.05,2.9 -2.5,-0.72 -5.07,-1.22 -7.54,-2.04 -11.78,-3.97 -14.72,-13.16 -14.2,-24.34 0.17,-3.45 -0.14,-6.85 -1.16,-10.15 -0.4,-1.32 -1.14,-2.53 -2.7,-2.4 -1.6,0.16 -2.1,1.5 -2,2.9 0.28,3.7 -1.95,9.37 -7.6,10.37C121,537 97.3,525.3 92.73,505.8c-1.64,-1.5 -3.04,-1.64 -4.8,-0.98 -1.07,0.4 -2.16,0.86 -3.04,1.55 -9.6,7.47 -20.4,8.52 -32.02,6.66 -6.1,-0.98 -11.44,-3.3 -15.46,-7.65 -9.14,-9.86 -13.94,-21.8 -14.97,-35.23 -0.2,-2.55 0.46,-4.92 2.46,-6.64 2.48,-2.13 4.33,-2.62 6.5,1.02 3.45,5.8 3.97,12.56 7.33,18.4 7.13,12.36 22.7,17.5 35,2.98 4.64,-5.47 4.54,-10.5 3.78,-16.98 -2.33,-20.22 -1.35,-40.16 4.53,-60.02 5.46,-18.4 13.88,-35.12 23.95,-51.2 5.25,-8.37 12.45,-15.13 20.4,-20.88 4.24,-3.1 10.25,-2.7 14.76,-5.88 1.4,-1 2.28,0.94 2.98,1.85 4.92,6.36 11.94,8.28 19.36,9.05 9.7,1 15.67,-4.7 19.8,-12.45 2.16,-4.04 3.35,-4.27 6.72,-1.1 3.13,2.94 6.97,5.32 11.15,6.9 1.4,0.54 2.73,1.28 4.2,0.65 1.43,-0.6 3.6,-0.34 4,-2.33 0.44,-2.22 -1.8,-2.45 -3.1,-3.13 -5.34,-2.84 -10.44,-6.04 -15.34,-9.6 -1.9,-1.4 -2.3,-2.74 -1.52,-4.94 3.85,-10.7 2.88,-21.9 2.96,-32.94 0,-2.66 -1.03,-5.73 -4.37,-5.4 -3.42,0.34 -2.4,3.4 -2.48,5.83 -0.34,10.57 0.2,48.52 -20.4,51.8 -5.6,0.88 -11.72,-3.87 -14.97,-7.55 -1.05,-1.2 -1.6,-2.82 -2.3,-4.3 -1.9,-3.97 -4.7,-4.7 -8.32,-2.28 -1.6,1.1 -3.16,2.3 -4.77,3.42 -6.07,4.18 -11.02,2.55 -13,-4.62 -2.12,-7.58 -2.73,-15.4 -2.9,-23.25 -0.18,-7.1 -0.37,-7.44 -7.23,-7.38 -3.63,0.02 -3.4,-2.65 -3.34,-4.76 0.3,-7.84 1.16,-15.63 3.16,-23.24 5.44,-20.78 15.25,-39.46 27.34,-57.06 9.98,-14.53 20.6,-28.52 32.6,-41.46 1.35,-1.46 2.53,-3.08 1.88,-5.25 -3.8,-12.62 -4.97,-25.52 -4.34,-38.64 0.18,-3.68 1.14,-7.06 3.14,-10.16 4.82,-7.47 11.9,-8.46 18.45,-2.54 4.23,3.82 5.38,3.75 7.42,-1.7 4.34,-11.62 13.95,-18.16 23.3,-25 1.14,-0.85 2.63,-1.3 4.03,-1.74 8.2,-2.65 11.25,-0.47 11.25,8.02 0,1.15 -0.07,2.3 -0.03,3.43 0.13,4.47 1.23,5.28 5.4,3.6 3.7,-1.5 7.53,-2.1 11.46,-2.12 2.42,0 5.3,-0.08 6.36,2.63 1,2.62 -0.22,5.13 -1.9,7.25 -1.02,1.28 -2.27,2.37 -3.23,3.67 -1.26,1.72 -1.58,3.66 -0.2,5.45 1.3,1.7 3.07,1.46 4.9,0.9 2.35,-0.7 4.7,-1.3 7.08,-1.85 5.5,-1.3 10.67,-0.84 16.25,0.93 22.57,7.14 39.55,21.56 53.67,39.85 10.63,13.77 21.63,27.04 37.92,34.54 8.35,3.85 16.9,6.37 26.37,6.2 15.5,-0.27 22.1,-14.73 14.75,-28.12 -2.35,-4.27 -5.4,-8.16 -8.04,-12.27 -3.88,-6 -10.2,-16.04 -5.6,-23.58 3.3,-5.45 5.37,-11.58 2.52,-18.9 -0.08,-1.6 -0.05,-3.35 0.46,-4.83 1.42,-4.12 5.42,-4.9 8.47,-1.8 1.54,1.56 2.3,3.5 2.8,5.62 0.98,4.25 2.27,8.28 7.44,9.2 5.6,0.97 10.84,0.58 14.68,-4.23 1.48,-1.85 3.28,-3.16 5.2,-4.4 1.44,-0.92 2.98,-1.45 4.56,-0.33 1.6,1.12 1.42,2.82 1.23,4.46 -0.2,1.65 -0.9,3.1 -1.9,4.44 -5.77,7.8 -5.23,16.43 2.02,22.87 23.06,20.5 30.33,46.86 26.25,76.12 -2.65,18.97 -11.78,35.84 -25.73,49.58 -2.2,2.18 -4.45,4.3 -6.68,6.46 -2.33,2.23 -3.93,2.15 -4.3,-1.4 -0.36,-3.5 -1.98,-6.5 -3.52,-9.58 -2.34,-4.65 -6.03,-6.72 -11.1,-6.17 -3.3,0.34 -8.26,5.75 -9.05,9.47 -3.08,14.45 -13.34,23.87 -27.43,26.86 -15.9,3.36 -30.32,1.14 -43.77,-8.18 -3.5,-2.43 -7,-4.17 -9.73,-8 -5.08,-7.13 -16.4,-6.98 -23.1,-0.75 -0.6,0.56 -1.48,1 -1.12,1.93 0.48,1.22 1.7,0.8 2.55,0.65 0.94,-0.16 1.8,-0.77 2.72,-1.1 5.43,-1.83 7.9,-1.44 13.27,2.3 -3.13,1.38 -6.1,2.42 -8.8,3.9 -15.9,8.8 -23.13,20.6 -18.48,36.6 1.56,5.37 3.22,10.8 8.85,13.87 3.24,1.77 2.84,3.92 -0.62,5.42 -3.44,1.5 -6.83,3.12 -10.25,4.65 -0.87,0.4 -2.22,0.47 -2.1,1.67 0.17,1.43 1.6,1.47 2.7,1.43 5.6,-0.17 11,-1.56 16.26,-3.22 3.9,-1.24 7.4,-1.2 11.37,0 12.8,3.8 23.85,4.72 37.07,3.7 4.57,-0.36 5.7,-1.85 5.04,6.07 -1.6,11.56 0.48,21.77 5.14,32.76 1.3,5.25 1.8,10.5 0.6,15.88 -0.85,3.8 -1.32,7.7 -1.27,11.6 0.03,2.27 -0.54,4.24 -3.2,4.77 -2.78,0.83 -2.32,3.13 -2.37,5.22 -0.17,7.17 -4.74,22.1 -8.8,28.5 -2.3,-2.82 -2.4,-6.32 -3.54,-9.36 -1.73,-4.62 -3.83,-9 -6.77,-12.94 -0.88,-1.17 -2.2,-2.94 -4.04,-1.66 -1.7,1.18 -0.24,2.68 0.23,3.94 9.64,25.54 15.33,51.83 14.55,79.25 -0.1,4.02 -1.04,8.04 -1.04,12.12 0,1.86 -0.92,2.67 -2.87,3.2 -11.3,3.13 -14.4,8.67 -11.5,20.46 0.5,1.98 -0.13,2.68 -1.68,3.32 -1.76,0.73 -2.25,-0.3 -2.74,-1.7 -0.43,-1.2 -1,-2.42 -1.74,-3.48 -3.22,-4.67 -8.8,-6.6 -13.74,-4.87 -5.05,1.8 -8.96,7.98 -8.8,14.03 0.02,1.28 0.7,3.1 0.1,3.75 -1.44,1.55 -3.4,1.9 -4.72,-0.53 -0.85,-1.58 -1.54,-3.24 -2.5,-4.73 -5.35,-8.23 -16.2,-9.3 -21.15,-0.74 -2.25,3.88 -3.3,5.1 -7.07,3.8 -5.44,-1.85 -10,-4.88 -13.63,-9.14 -3.86,-4.55 -2.3,-10.3 -2.3,-15.55 0.1,-20.53 1.55,-41.06 -0.07,-61.6 -0.5,-6.54 -2.04,-12.88 -3.3,-19.27 -0.45,-2.2 -0.6,-5.1 -3.8,-4.7 -3.36,0.44 -2.14,3.43 -2.3,5.55 -0.04,0.48 0.06,0.97 0.1,1.46 -1.97,6.44 -11.3,12.66 -17.7,11.78z" + android:fillColor="@color/elephant_friend_body_color" + android:fillType="evenOdd"/> + <path + android:pathData="M370.76,415.57c1,-8.35 2,-16.7 2.98,-25.04 0.28,-2.4 -0.07,-4.88 0.88,-7.2 11.63,18.26 23.94,36.14 34.7,54.9 9.17,16 15.53,33.42 21.23,51 0.55,1.68 0.68,2.63 -1.66,2.85 -12.2,1.16 -16.57,7.23 -13.92,18.97 0.13,0.56 0.02,1.18 0.02,1.8 -2.04,0.28 -2.4,-1.26 -3.07,-2.23 -3.37,-4.92 -7.88,-6.6 -13.62,-5.1 -5.52,1.4 -9.5,4.85 -10.5,10.37 -0.68,3.88 -2.28,4.43 -5.52,3.85 -4.17,-0.74 -8.36,-1.36 -12.56,-1.97 -2.54,-0.37 -3.56,-1.5 -3.3,-4.3 1.32,-14.88 0.22,-29.66 -2.8,-44.27 -0.6,-2.87 -0.3,-5.22 0.94,-7.93 5.26,-11.42 10.02,-23.04 10.24,-35.9 0.07,-3.8 0,-7.65 -4.04,-9.8z" + android:fillColor="@color/elephant_friend_dark_body_color_2" + android:fillType="evenOdd"/> + <path + android:pathData="M356.08,338.47c-7.7,0.43 -18.2,-2.36 -28.64,-4.5 -16.86,-3.44 -25.54,-20.38 -18.15,-35.16 4.42,-8.87 12.58,-13.95 22.5,-15.12 4.26,-0.5 7.23,3.35 10.35,5.82 5.75,4.55 12,8.16 19.3,8.75 14.3,1.16 28.3,-0.6 41.18,-7.27 9.55,-4.95 15.58,-13.08 18.27,-23.62 0.7,-2.78 1.5,-6.7 4.48,-6.78 3.67,-0.1 4.9,3.98 5.65,7.1 4.6,19.4 -3.52,40.9 -19.7,52.66 -6.42,4.68 -12.42,10.08 -20,12.93 -10.3,3.88 -21.1,5.1 -35.25,5.2z" + android:fillColor="@color/elephant_friend_light_color_2" + android:fillType="evenOdd"/> + <path + android:pathData="M440,305c-3.6,-0.1 -3.5,-1.06 -3.58,-5.14 -0.32,-13.9 7.15,-20.92 14.32,-27.6 5.77,-5.37 11.57,-8.7 19.6,-9.65 8.85,-1.02 16.66,-5.3 22.36,-12.68 0.9,-1.16 1.73,-3.32 3.63,-2.48 1.62,0.72 2.38,2.66 2.6,4.5 0.92,7.78 -0.24,15.26 -3.5,22.4 -7.07,15.62 -27.1,24.16 -36.08,27 -6.98,2.23 -15.14,3.76 -19.34,3.65z" + android:fillColor="@color/elephant_friend_light_color_1" + android:fillType="evenOdd"/> + <path + android:pathData="M237.55,461.68c6.07,-3.67 12.54,-6.74 17.7,-11.77 0.9,9.37 1.87,18.73 2.6,28.1 0.43,5.35 0.5,10.73 0.62,16.1 0.04,1.77 -0.06,3.6 -0.48,5.32 -0.7,2.85 -3.2,4.6 -5.63,4.7 -2.56,0.13 -1.87,-2.9 -2.13,-4.66 -2,-13.35 -6.65,-25.8 -12.7,-37.78zM31.56,456.6c-3.18,-1.1 -5.75,0.44 -8.2,2.1 -4.54,3.08 -6.36,2.92 -9.92,-1.35 -0.93,-1.1 -1.68,-2.4 -2.42,-3.65 -0.5,-0.84 -1,-1.88 -0.34,-2.75 0.95,-1.27 2.02,-0.18 3.06,0.1 3,0.82 6.4,4.16 8.85,2.05 2.36,-2.03 -1.6,-4.94 -2.08,-7.75 -0.7,-4 -1.72,-7.94 -1.6,-12.06 0.1,-3.6 2.05,-5.86 5.37,-6.68 3.62,-0.88 4.72,2.2 5.97,4.58 1.4,2.67 1.2,5.7 1.4,8.62 0.12,1.73 -0.6,3.82 1.47,4.78 1.8,0.84 2.96,-0.8 4.32,-1.6 1.54,-0.9 3.1,-1.96 4.77,-2.4 3.37,-0.88 5.83,1.87 4.8,5.12 -0.7,2.13 -13.23,10.96 -15.47,10.9z" + android:fillColor="@color/elephant_friend_dark_body_color_2" + android:fillType="evenOdd"/> + <path + android:pathData="M320.74,552.86c0,-3.62 1.58,-6.17 3.95,-8.34 1.97,-1.8 4.1,-1.73 6.32,-0.54 2.6,1.4 3.5,4.16 3.66,6.55 0.17,2.4 -2.78,2.16 -4.55,2.68 -2.18,0.65 -4.42,1.1 -6.62,1.7 -2.04,0.53 -3.18,0.08 -2.76,-2.04zM211.12,555.75c0.62,-7.63 5.97,-12.05 11,-9.22 1.87,1.04 3.73,2.48 3.63,4.83 -0.1,2.32 -9,5.34 -11.57,6.4 -2,0.8 -3.7,0.78 -3.06,-2zM190.44,562.93c-1.76,-0.23 -10.18,0.22 -10.6,-2.52 -0.35,-2.33 1.4,-4.48 3.5,-6 2.27,-1.65 4.47,-1.77 6.75,-0.26 1.06,0.7 2.23,1.4 3.02,2.37 1.1,1.33 3.2,2.64 2.36,4.47 -0.8,1.78 -2.98,2.23 -5.04,1.96zM391.17,519.44c2.04,-6.24 7,-9.52 11.73,-8.35 2.15,0.53 3.55,2.03 3.88,3.9 0.42,2.34 -2.14,1.87 -3.42,2.47 -3.73,1.76 -7.83,1.28 -12.2,1.97zM302.64,557.6c-3.28,-0.47 -11.57,-0.97 -11.9,-2.4 -0.42,-1.8 1.33,-3.16 2.75,-4.2 4.6,-3.4 8.78,-2.1 10.7,3.23 1,2.82 0.55,3.43 -1.56,3.36zM420.1,510.42c-1.2,-5.6 0.16,-9.25 4.92,-11.44 1.9,-0.88 3.66,-0.63 4.82,0.74 1.2,1.43 -0.37,2.67 -1.28,3.7 -2.34,2.63 -4.96,4.94 -8.47,7zM356.97,530.9c-0.27,4.6 -2,7.9 -5.37,10.37 -0.64,0.46 -1.24,1.42 -2,0.94 -0.53,-0.33 -0.82,-1.35 -0.9,-2.1 -0.38,-4.45 3.08,-8.4 8.27,-9.2zM242.28,535.5c-0.73,3.33 -1.4,6.46 -4.55,7.58 -0.62,0.22 -0.98,-2.36 -0.68,-3.82 0.53,-2.58 1.12,-5.3 4.13,-5.85 1.8,-0.32 0.86,1.56 1.1,2.1z" + android:fillColor="@color/elephant_friend_light_color_1" + android:fillType="evenOdd"/> + <path + android:pathData="M369.34,395.2c-0.36,10.82 -7.26,15.35 -17.1,11.05 -1.5,-0.65 -2.08,-0.66 -3.2,0.53 -7.24,7.7 -14.98,6.93 -20.1,-2.23 -1.5,-2.67 -2.45,-3.12 -5.43,-1.97 -7.26,2.82 -11.64,-0.6 -11.53,-8.5 0.06,-4.7 1.6,-8.96 4.12,-12.87 0.8,-1.26 2,-2.45 3.64,-1.64 1.92,0.95 0.28,2.3 -0.08,3.3 -1.25,3.6 -2.2,7.2 -2.02,11 0.18,3.53 2.2,3.96 4.5,2.1 2.1,-1.7 3.7,-4.08 5.5,-6.2 0.75,-0.88 1.4,-1.87 2.8,-1.45 1.32,0.4 1.52,1.5 1.67,2.7 0.63,4.87 2.5,9.06 6.5,12.16 2.58,2 4.4,1.93 5.47,-1.45 0.74,-2.32 1.6,-4.6 2.26,-6.96 0.8,-2.9 2.17,-3.03 4,-0.92 2.32,2.68 5.2,5.96 8.57,3.67 3.46,-2.35 1.47,-6.54 0.3,-9.9 -0.9,-2.64 -2.05,-5.16 -4,-7.24 -0.8,-0.84 -1.68,-1.75 -0.73,-2.96 0.7,-0.9 1.7,-0.6 2.7,-0.43 4.32,0.73 7.47,2.7 9.14,7.06 1.4,3.65 3.27,7.1 3.04,11.17zM257.8,234.95c0,3.1 0.06,5.4 -0.03,7.67 -0.05,1.25 -0.75,2.13 -2.06,2.38 -1.4,0.27 -2.43,-0.06 -3.33,-1.34 -8.95,-12.77 -2.18,-30.7 12.84,-33.9 7.24,-1.53 18.57,3.32 22.34,9.53 1.63,2.66 0.77,5.45 -0.96,7.1 -2.03,1.94 -3.08,-1.2 -4.62,-2.1 -3.3,-1.95 -6.58,-4.33 -10.2,-5.3 -6.54,-1.75 -12.92,3.24 -13.6,9.93 -0.24,2.27 -0.28,4.56 -0.36,6.02zM335.63,182.77c-0.4,3.9 0.7,6.67 1.5,9.5 0.5,1.72 0.1,3.34 -1.5,4.28 -1.32,0.8 -2.77,0.04 -3.48,-0.96 -3.8,-5.36 -6.13,-11.28 -4.57,-17.9 1.37,-5.78 5.48,-9.1 11.26,-10.22 7.58,-1.47 16.86,5.04 17.3,12.13 0.1,1.34 0.82,3.13 -1.05,3.9 -1.44,0.58 -2.48,-0.47 -3.45,-1.34 -1.58,-1.4 -3,-3.04 -4.72,-4.25 -2.7,-1.88 -5.8,-2.27 -8.88,-1.06 -3.2,1.27 -2.16,4.37 -2.42,5.93z" + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd"/> + <path + android:pathData="M334.32,456.65c0.4,3.22 -0.48,5.42 -2.9,6.2 -2.62,0.85 -4.6,-1 -5.16,-3.3 -1.23,-5.03 -2.02,-10.18 -2.83,-15.3 -0.3,-1.87 0.14,-3.75 2.42,-4.15 2.12,-0.36 3.48,1 4.1,2.76 1.63,4.77 3.06,9.6 4.38,13.8zM346.88,497.52c-0.37,3.22 -1.74,5.15 -4.26,5.35 -2.76,0.2 -4.25,-2.07 -4.25,-4.42 -0.02,-5.18 0.42,-10.37 0.83,-15.55 0.15,-1.88 1,-3.6 3.32,-3.45 2.14,0.14 3.16,1.77 3.33,3.64 0.48,5 0.73,10.04 1.03,14.42zM111.33,405.23c-1.5,2.88 -3.47,4.2 -5.9,3.47 -2.64,-0.8 -3.22,-3.44 -2.4,-5.64 1.85,-4.85 4.12,-9.54 6.35,-14.23 0.82,-1.7 2.23,-3 4.34,-2.03 1.95,0.9 2.3,2.78 1.8,4.6 -1.34,4.84 -2.9,9.64 -4.2,13.83zM125.06,433.6c-1.18,3.04 -3,4.56 -5.47,4.1 -2.74,-0.5 -3.6,-3.06 -3,-5.34 1.28,-5.02 3.02,-9.93 4.73,-14.83 0.62,-1.78 1.9,-3.23 4.1,-2.5 2.03,0.68 2.6,2.52 2.3,4.37 -0.82,4.97 -1.85,9.9 -2.67,14.22zM318.95,487.36c-0.07,3.25 -1.26,5.3 -3.74,5.72 -2.73,0.47 -4.42,-1.66 -4.64,-4 -0.5,-5.16 -0.55,-10.37 -0.6,-15.56 -0.04,-1.88 0.65,-3.68 2.97,-3.74 2.15,-0.07 3.3,1.47 3.66,3.3 0.92,4.96 1.65,9.95 2.35,14.28zM220.34,466.9c2.03,2.55 2.43,4.9 0.8,6.8 -1.8,2.1 -4.47,1.56 -6.14,-0.1 -3.7,-3.63 -7.06,-7.6 -10.44,-11.54 -1.23,-1.44 -1.85,-3.26 -0.1,-4.8 1.6,-1.42 3.47,-0.98 4.92,0.2 3.9,3.2 7.65,6.58 10.96,9.45zM218.05,505.25c0.7,3.18 0.02,5.45 -2.3,6.45 -2.54,1.1 -4.7,-0.58 -5.46,-2.8 -1.7,-4.9 -2.98,-9.95 -4.26,-14.98 -0.47,-1.83 -0.22,-3.74 2.02,-4.34 2.07,-0.57 3.56,0.65 4.33,2.36 2.07,4.6 3.94,9.27 5.65,13.3z" + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd"/> + <path + android:pathData="M146.02,376.7c-1.1,4.03 -2.14,8.66 -3.63,13.13 -0.78,2.3 -2.78,3.84 -5.43,2.93 -2.74,-0.93 -3.6,-3 -2.78,-5.86 1.37,-4.75 3.28,-9.23 6.55,-13 0.98,-1.14 2.27,-2.02 3.87,-1.16 1.32,0.7 1.5,2.04 1.4,3.95zM133.8,239.83c1.24,-5.52 3.77,-10.15 7.84,-13.9 1.08,-1 2.33,-1.1 3.57,-0.23 1.22,0.86 1.73,2.12 1.23,3.43 -1.82,4.68 -3.67,9.36 -5.76,13.93 -0.7,1.54 -2.42,2.3 -4.18,1.58 -2.17,-0.88 -2.2,-3.04 -2.7,-4.8z" + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd"/> + <path + android:pathData="M364.47,208.65c0.68,-3.4 2.7,-5.58 4.54,-7.8 2.75,-3.3 6.58,-5.2 10.03,-7.6 1.67,-1.14 3.5,-1.1 4.63,0.8 1.1,1.9 0.32,3.47 -1.46,4.5 -5.08,2.98 -9.5,6.58 -12.3,11.93 -0.7,1.33 -1.87,2.42 -3.55,1.87 -1.97,-0.65 -1.72,-2.5 -1.88,-3.7z" + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd"/> + <path + android:pathData="M162.8,248.16c-0.42,3.7 -0.44,6.67 -1.2,9.44 -0.72,2.68 -2.34,5.54 -5.74,4.5 -2.95,-0.9 -4,-3.72 -3.22,-6.74 0.8,-3.12 1.53,-6.34 3.8,-8.8 1.1,-1.2 2.46,-2.33 4.26,-1.6 1.73,0.7 2.12,2.3 2.1,3.2z" + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd"/> + <path + android:pathData="M190.34,490.35c0.05,2.55 -0.43,4.5 -2.64,5.16 -2.07,0.64 -3.58,-0.8 -4.3,-2.45 -1.66,-3.87 -3.07,-7.86 -4.44,-11.86 -0.5,-1.44 -0.36,-3 1.1,-3.93 1.6,-1 2.87,-0.02 3.84,1.12 3.1,3.62 5.57,7.6 6.44,11.95zM130.17,363.58c-2.9,-0.2 -4,-2.12 -3.06,-4.66 2,-5.3 6.8,-8.15 11.1,-11.2 1.7,-1.2 3.38,0.36 3.4,2.66 0,2.32 -8.13,12.54 -10.33,13.05 -0.32,0.08 -0.64,0.1 -1.1,0.15z" + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd"/> + <path + android:pathData="M179.53,216.1c-0.84,4 -2.6,7.82 -5.46,11.05 -1.5,1.7 -3.52,1.57 -5.38,0.5 -1.93,-1.1 -2.08,-2.97 -1.26,-4.7 1.84,-3.85 4.4,-7.22 7.88,-9.78 0.92,-0.67 2.04,-1.17 3.18,-0.4 1.08,0.74 1.1,1.9 1.03,3.32z" + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd"/> + <path + android:pathData="M175.5,186.16c-1.3,3.68 -3.26,7.4 -6.47,10.23 -1.24,1.1 -2.8,0.47 -4.02,-0.48 -1.37,-1.08 -2.35,-2.67 -1.32,-4.22 2.05,-3.08 3.98,-6.37 7.17,-8.44 2.43,-1.57 4.7,-0.2 4.65,2.9z" + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd"/> + <path + android:pathData="M386.26,211.43c0.38,-4.44 1.16,-8.6 3.12,-12.42 0.98,-1.9 2.6,-3.23 4.77,-2.26 2.48,1.1 1.68,3.02 0.77,4.95 -1.66,3.53 -3.06,7.2 -4.73,10.72 -0.57,1.18 -0.87,3.34 -2.8,2.72 -1.68,-0.54 -0.95,-2.52 -1.14,-3.7z" + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd"/> + <path + android:pathData="M404.98,200.06c0.27,-2.52 0.07,-5.14 2.68,-5.6 3.04,-0.52 2.94,2.3 3.4,4.27 0.53,2.2 1,4.43 1.52,6.64 0.44,1.85 0.72,3.75 -1.6,4.38 -2.45,0.67 -4.13,-0.43 -4.68,-2.94 -0.52,-2.37 -0.94,-4.76 -1.32,-6.74z" + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd"/> +</vector> diff --git a/app/src/main/res/drawable/elephant_friend_empty.xml b/app/src/main/res/drawable/elephant_friend_empty.xml new file mode 100644 index 0000000..1a67e09 --- /dev/null +++ b/app/src/main/res/drawable/elephant_friend_empty.xml @@ -0,0 +1,113 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="140dp" + android:height="159dp" + android:viewportWidth="510" + android:viewportHeight="580"> + <!-- Speech bubble background --> + <path + android:fillColor="@color/elephant_friend_accent_color" + android:fillType="evenOdd" + android:pathData="M394.28,157.1c-7.35,-6.5 -13.58,-14.2 -15.95,-23.65 -10.08,-40.07 20.35,-70.8 51.32,-74.1 24.2,-2.57 47.07,6.67 60.52,22.02 27.04,30.86 16.45,74.4 -14.43,91.4 -19.06,10.46 -40.3,3.45 -46.95,3.2 -3.42,-0.13 -25.6,9.5 -32.5,13.88 -1.68,1.07 -3.52,1.98 -5.42,0.56 -2,-1.47 -1.18,-3.47 -0.7,-5.36 0.5,-2.05 6.17,-26.13 4.1,-27.95z" /> + <!-- Border, shadow --> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M415.6,303.76c-1.72,1.5 -3.38,3.05 -5.08,4.58 -3.58,3.24 -14.12,2.94 -17.7,-0.44 -2.15,-2.03 -1.74,-3.02 1.04,-3.88 3.88,-1.2 7.66,-2.73 11.47,-4.17 15.35,-5.8 24.8,-17.05 28.94,-32.6 2.37,-8.9 -1.8,-13.55 -11,-13.94 -7.06,-0.3 -14,0.78 -20.95,1.45 -9.33,0.9 -18.55,0.57 -27.53,-2.26 -7.48,-2.37 -10.93,-7.27 -11.57,-14.84 -1.13,-13.3 -4.25,-33.32 -2.67,-39.85 1.52,-6.28 7,-20.6 5.78,-30.26 -1.18,-9.38 -4.67,-14.27 -7.74,-19.85 -3.2,-5.83 -5,-12.75 -6.97,-18.9 -5.04,-15.65 -11.93,-30.6 -23.7,-42.74 -7.07,-7.28 -15.15,-13.3 -22.8,-19.87 -5.32,-4.6 -11.2,-7.82 -18,-9.3 -3.1,-0.65 -4.23,-1.47 -3.4,-5.17 2.3,-10.25 -3,-16.46 -13.4,-16.5 -1.46,-0.02 -2.94,-0.08 -4.4,0.1 -3.8,0.48 -5.26,-0.44 -4.23,-4.78 2.02,-8.56 -4.18,-16.05 -12.95,-16.12 -6.58,-0.04 -13.02,0.92 -19.02,3.7 -2.42,1.12 -3.52,1.1 -4.72,-1.66 -2.43,-5.58 -6.8,-8.86 -13.04,-9.75 -12.25,-1.75 -22.9,1.67 -32.07,9.7 -9.78,8.53 -16.37,19.45 -21.2,31.25 -4.1,10.04 -9.46,18.37 -19.75,22.88 -2.8,1.23 -5.35,3.12 -7.84,4.95 -20.45,15 -36.96,33.6 -50.95,54.7 -9.9,14.93 -18.4,30.2 -22.3,47.94 -2.72,12.42 -5.2,24.87 -7.12,37.4 -2,12.93 -1.06,25.64 3.62,37.97 2.12,5.62 5.57,8.77 11.06,8.6 4.34,-0.16 5.3,1.77 6.14,5.52 2.27,9.9 5.9,19.05 17.13,22.2 1.76,0.5 2.48,1.3 0.87,3.05 -5.94,6.5 -11.04,13.64 -16.03,20.9 -10.44,15.25 -17.88,31.82 -21.53,49.87 -1.94,9.55 -2.54,19.4 1.98,28.76 1.06,2.18 1.1,4.64 1,7.16 -0.87,19.15 7.85,33.9 21.43,46.83 -7.8,4.47 -26.34,3.1 -32.96,-2.53 -1.27,-1.08 -2.46,-2.18 -1.26,-4.12 4.77,-7.68 1.6,-14 -3.58,-19.87 -5.1,-5.77 -9.94,-5.4 -14.1,1.15 -1.15,1.8 -2.08,2.9 -4.55,2.6 -5.7,-0.7 -9.83,2.58 -12.45,7.02 -2.95,4.97 -3.6,10.74 0.2,15.65 2.4,3.1 3.04,5.15 0.5,8.66 -3.02,4.2 -2.42,10.07 0.2,12.76 3.56,3.66 9.77,4.7 14.45,2.76 7.66,-3.16 5.8,-8.13 7.66,-8.3 1.86,-0.16 3.38,3.87 11.95,7.48 17.15,7.23 35.34,7.46 52.57,-1.77 2.18,-1.17 3.87,-1 4.3,1.77 0.73,4.83 1.38,9.6 0.26,14.54 -2.64,11.65 -2.5,23.3 1.52,34.64 1.13,3.2 0.5,4.16 -2.76,4.9 -9.27,2.17 -28.52,13.96 -20.37,24.95 12.32,16.6 64.27,23.74 88.46,25.13 17.28,1 80.47,1.1 98.78,-0.78 54.86,-5.65 104.97,-17.47 110.52,-31.5 5.54,-14.06 -18.83,-24.9 -24.63,-25.7 -3.63,-0.5 -3.84,-2.1 -3.06,-4.9 0.93,-3.28 1.96,-6.56 2.6,-9.9 2.88,-14.93 3.25,-30.08 3.88,-45.2 0.08,-1.93 -0.33,-3.98 1.2,-5.56 7.33,-7.6 11.5,-16.9 14.65,-26.76 0.9,-2.82 2.78,-4.44 5.44,-5.06 8.8,-2.05 16.87,-5.88 24.88,-9.88 5.4,-2.7 10.54,-5.68 14.96,-9.98 11.36,-11.03 10.58,-23.68 5.24,-36.94 -1.17,-2.9 -1.2,-5.5 -0.62,-8.5 1.65,-8.72 4.06,-16.95 9.53,-24.26 4.17,-5.6 6.2,-11.95 4.07,-19.32 -2.68,-9.35 -10.94,-12.2 -18.26,-5.84zM245.8,512.6c-3.24,-0.14 -27.22,-0.8 -39.35,0.44 -2.15,0.22 -3.5,-0.3 -3.46,-2.75 0.1,-5.04 -0.16,-10.08 1.02,-15.05 0.55,-2.33 1.74,-3.4 4.08,-3.14 12.18,1.3 24.3,1.13 36.34,-1.43 2.5,-0.53 3.15,0.9 3.32,2.95 0.4,5.18 0.75,10.37 1.3,15.54 0.26,2.6 -0.7,3.54 -3.25,3.43z" /> + <!-- Light body part --> + <path + android:fillColor="@color/elephant_friend_body_color" + android:fillType="evenOdd" + android:pathData="M279.22,471.42c-10.37,3.92 -21.02,7 -31.83,9.33 -14.18,3.05 -28.5,4.7 -42.93,1.27 -5.1,-1.22 -6.84,0.05 -7.17,5.5 -0.5,8.62 -0.68,17.25 -0.92,25.87 -0.07,2.6 -0.5,5.14 -1.2,7.66 -0.4,1.4 -0.9,2.16 -2.47,2.5 -9.43,2.06 -13.78,7.97 -13.03,17.65 0.12,1.5 0.77,2.9 -1.3,3.54 -2.36,0.73 -4.25,1.83 -5.14,-2.2 -1.5,-6.9 -6.23,-10.83 -11.5,-10.77 -5.86,0.08 -9.53,3.6 -10.55,10.03 -0.24,1.52 0.85,4.24 -2.22,3.76 -2.34,-0.36 -5.53,-0.18 -5.45,-4.13 0.06,-2.64 -0.68,-5.14 -1.8,-7.54 -1.95,-4.13 -4.66,-7.13 -9.7,-6.64 -4.93,0.47 -9.5,1.56 -10.54,7.5 -0.5,2.9 -1.83,2.98 -3.95,1.46 -5.93,-4.24 -8.36,-10.68 -10.12,-17.25 -3.76,-14.07 -2.06,-28.3 1.36,-42.02 2.3,-9.23 -2.2,-14.2 -7.85,-19.44 -1.4,-1.28 -2.67,-1.77 -4.13,-0.13 -5.28,5.96 -12.45,8.2 -19.9,8.84 -8.02,0.72 -16.07,1.53 -24.25,-0.27 -6.38,-1.4 -11.9,-4.15 -17.35,-7.4 -3.48,-2.1 -4.26,-5.15 -2.8,-8.83 1.38,-3.48 4.4,-4.22 7.3,-1.8 11.86,9.9 28.6,11.16 42.12,3.2 3.7,-2.2 3.57,-4.12 0.4,-7.03 -10.24,-9.45 -17.1,-20.83 -19.24,-35.38 3.4,1.25 5.04,4.06 7.54,5.66 14.7,9.48 30.9,15.2 48.3,14.54 13.2,-0.52 29.47,-8.72 42.15,-28.47 7.02,-10.93 11.06,-22.32 15.47,-33.98 1.04,-2.74 1.8,-5.58 2.8,-8.33 1.6,-4.3 1.66,-8.3 -2.84,-12.08 -1.7,8.8 -5.64,16.5 -8.8,24.53 -5.66,14.4 -11.54,28.77 -22.86,39.94 -1.1,1.1 -3.88,-1.6 -3.95,-3.93 3.65,-4.67 4.83,-9.88 2.3,-15.56 -1.97,-4.45 -5.16,-8.2 -10.13,-7.87 -7.44,-0.32 -11.44,10.3 -12.8,11.2 -1.86,-0.5 -2.58,-0.82 -2.46,-1.82 0.4,-3.55 2.6,-8 2.08,-11.6 -0.92,-6.28 -4.25,-10.2 -9.75,-11.28 -5.4,-1.07 -11.47,1.53 -14.34,6.26 -0.84,1.38 -1.52,2.9 -2.1,4.4 -0.53,1.48 -0.37,3.35 -3,2.56 -2.2,-0.66 -2.6,-1.76 -2.32,-3.95 0.67,-5.1 0.43,-10.17 -3.16,-14.32 -3.74,-4.32 -8.46,-5.23 -13.74,-3.3 -5.14,1.87 -9.4,8.73 -9.46,13.83 -0.03,3.12 -1.9,3.1 -3.72,3.2 -2.9,0.15 -1.8,-2 -1.6,-3.46 1.55,-12.6 6.18,-23.93 12.3,-35.16 7.56,-13.88 15.37,-27.5 24.33,-40.52 3.64,-5.3 7.7,-10 13.6,-12.97 2.14,-1.1 3.23,-1.07 4.94,0.74 9.82,10.4 29.58,8.63 38.24,-2.8 7.92,-10.48 14.4,-21.63 18.4,-34.13 3,-9.4 3,-19.24 3.22,-29 0.1,-4.7 0.27,-9.42 -0.46,-14.13 -0.26,-1.68 -0.9,-3.1 -1.6,-4.55 -0.45,-0.93 -1.17,-1.63 -2.3,-1.46 -1.43,0.23 -1.72,1.4 -1.7,2.58 0.16,8.32 -1.25,16.54 -1.64,24.8 -0.65,13.9 -6.48,26.08 -11.67,38.5 -1.6,3.86 -4.14,7.36 -6.28,11 -3.85,6.6 -13.65,11.12 -21.3,10.03 -3.9,-0.56 -6.38,-2.9 -8.54,-5.92 -5.6,-7.9 -5.98,-8 -14.55,-3.3 -3.66,2 -7.37,3.35 -11.6,3.13 -3.42,-0.2 -5.72,-1.64 -7.1,-4.96 -2.3,-5.65 -3.1,-11.6 -4.16,-17.53 -1.06,-5.9 -2.2,-6.55 -7.9,-5.17 -8.1,1.96 -11.05,0.07 -11.63,-8.2 -2.78,-39.67 3.82,-77.05 25.98,-111.02 12.12,-18.56 25.57,-35.9 43.66,-48.8 6.42,-4.56 12.15,-10.13 19.2,-13.87 4.24,-2.26 7.54,-5.7 9.85,-9.94 4.18,-7.73 8.3,-15.5 12.37,-23.28 4.6,-8.78 10.3,-16.5 19.04,-21.67 4.9,-2.9 10.07,-4.14 15.75,-4.52 9.55,-0.65 15.02,7.32 11.64,16.74 -0.55,1.54 -1.87,3.07 -0.17,4.32 1.53,1.1 2.7,-0.47 3.7,-1.48 5.87,-5.86 13.33,-8.38 21.15,-10.25 4.28,-1 8.1,-0.6 11.18,2.83 2.75,3.05 3.85,6.22 0.05,9.2 -2.7,2.1 -3.02,5.35 -4.1,8.1 -1.2,3.07 1.36,3.2 3.36,2.43 4.5,-1.76 9.2,-2.1 13.92,-2.26 3.64,-0.13 6.83,1.14 8.6,4.53 1.12,2.13 -2.03,7.8 -2.55,8.6 -2.68,4.28 -2.55,4.38 2.4,4.88 11.46,1.17 21.7,4.95 30.23,13.15 6.1,5.85 12.9,10.9 18.8,17.05 9.33,9.76 15.03,21.52 19.55,34.02 1.2,3.36 2.54,6.7 3.72,10.07 0.47,1.34 1.2,3.18 -1.25,3.23 -7.78,0.17 -10.77,6.4 -12.8,11.83 -6.8,18.36 -2.75,43.02 18.2,53.4 1.64,0.8 1.97,1.82 2.15,3.37 1.82,15.66 3.45,31.35 5.6,46.97 3.02,21.67 11.1,41.1 27.12,56.53 5.08,4.9 11.43,6.24 18.6,6.8 6.46,0.5 9.8,-2.7 13.6,-6.3 2.44,-2.3 4.12,-1.64 5.9,0.86 2.92,4.14 3.55,8.9 0.06,13.13 -6.72,8.2 -9.5,17.6 -10.53,27.86 -0.34,3.43 -1.5,6.7 -3.45,9.57 -5.98,8.87 -15.2,8.46 -19.86,-1.12 -2.25,-4.6 -5.2,-5.67 -10.27,-5.96 -19.05,-1.08 -38.06,-3.04 -54.03,-15.28 -1.02,-0.8 -2,-1.65 -3.35,-2.8 2.26,-1.4 4.25,-2.72 6.3,-3.93 6.63,-3.9 10.87,-9.54 12.57,-17.1 1.08,-4.8 -1.28,-8.54 -6.13,-9.8 -5.72,-1.48 -11.4,-0.1 -16.93,0.72 -18.5,2.75 -32.6,-4.43 -44.13,-18.2 -6.16,-7.37 -10.08,-15.85 -12.8,-25 -0.55,-1.9 -0.16,-3.45 0.5,-5.14 3.7,-9.7 -2.85,-16.26 -9.9,-20.88 -1.56,-1.02 -3.48,-0.86 -5.2,-0.12 -1.25,0.54 -2.23,1.47 -2.18,2.96 0.04,1.4 1.12,1.8 2.3,2.1 4.33,1.15 6.5,4.8 8.62,8.14 1.95,3.1 0.45,5.02 -3.23,4.96 -21.47,-0.33 -39.34,13.85 -42.72,33.88 -1.03,6.08 0.26,11.96 1.48,17.85 -3.6,2.1 -6.02,1.48 -10.24,-2.67 -2.8,-3.36 -7.06,-4.18 -11,-6.62 1.13,3.12 4.65,3.44 5.06,6.34 0.15,1.66 -0.32,3.14 -0.95,4.68 -2,4.92 -0.93,7.65 3.9,10.42 6.57,3.78 13.94,5.37 21.23,6.63 5.6,0.97 10.93,2.93 16.56,3.6 19.9,10.77 41.28,15.3 63.8,14.66 4.28,-0.12 8.17,-2.1 12.23,-3.25 2.4,-0.7 4.1,-0.62 6.13,1.18 9.17,8.16 19.8,13.5 31.98,15.55 1.77,0.3 3.5,0.77 5.27,1.05 3.67,0.57 5.1,2.5 5.96,6.33 1.93,8.66 3,17.3 2.7,26.13 -1.02,23.5 -12.04,42.17 -28.75,57.7 -5.67,5.26 -13.34,8 -20.2,11.65 -5.68,3.04 -11.88,5.26 -17.85,7.84 -5.6,2.4 -11.3,3.76 -17.4,3.02z" /> + <!-- Further (darker) leg --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_2" + android:fillType="evenOdd" + android:pathData="M279.22,471.42c10.28,-2.2 31,-8.76 39.53,-13.88 15.54,-9.33 30.38,-26.6 38.56,-45.9 2.8,-6.6 4.07,-13.63 6.14,-20.42 1.6,8.36 -0.26,16.5 -1.88,24.6 -2.68,13.42 -6.86,26.16 -18.13,35.28 -1.77,1.43 -4.2,3.32 -2.35,6.14 1.8,2.72 4.05,0.22 6.67,-0.2 -0.63,14.65 -0.92,29.07 -5.2,43.06 -0.4,1.3 -1.17,2.33 -2.4,2.93 -4.9,2.4 -6.57,6.43 -6.3,11.65 0.1,1.32 0.52,3.26 -1.26,3.83 -1.5,0.48 -2.1,-1.28 -3.03,-2.1 -8.05,-6.85 -17.14,-3.58 -19.1,7.03 -0.4,2.23 -1.14,3.6 -3.45,3.97 -2.78,0.45 -2.6,-1.84 -3.18,-3.4 -2.16,-5.7 -6.1,-8.9 -12.32,-9.58 -6.35,-0.7 -9.8,2.8 -12.1,7.83 -1.3,2.84 -3.1,3.34 -5.83,2.87 -11.17,-1.92 -13.98,-5.16 -14.82,-16.36 -0.4,-5.35 -1.23,-10.68 -2.2,-15.95 -0.7,-4.03 0.65,-6.12 4.68,-6.8 5.2,-0.85 9.8,-3.38 14.57,-5.45 4.48,-1.94 5.6,-5.36 3.4,-9.13z" /> + <!-- Closer (brighter) tusk --> + <path + android:fillColor="@color/elephant_friend_light_color_2" + android:fillType="evenOdd" + android:pathData="M249.68,262.06c3.5,-0.82 4.93,1.67 5.86,4.97 3.6,12.78 10.14,23.67 20.14,32.58 14,12.5 30.04,16.68 48.3,13.13 2.23,-0.44 4.53,-0.58 6.8,-0.57 4.1,0.04 5.33,2.14 3,5.52 -4,5.78 -8.6,10.78 -15.63,13.23 -7.17,2.52 -13.93,5.86 -21.84,6.23 -16.13,0.74 -32.02,-1.26 -46.68,-7.43 -12,-5.05 -23.18,-12.57 -32.14,-22.42 -3.85,-4.23 -2.73,-10.12 -2.02,-15.4 2.15,-15.97 17.82,-29.77 34.22,-29.82z" /> + <!-- Further (darker) tusk --> + <path + android:fillColor="@color/elephant_friend_light_color_1" + android:fillType="evenOdd" + android:pathData="M386.24,295.4c-3.06,0.45 -4.2,-0.34 -5.38,-1.74 -8.34,-10 -11.94,-21.93 -14.35,-34.37 -0.4,-2.1 0.76,-2.4 2.2,-1.6 9.53,5.12 19.88,5.2 30.3,5.42 5.46,0.12 10.57,-1.34 15.8,-2.42 1.9,-0.4 3.78,-1.05 5.7,-1.24 6.18,-0.6 9.65,3.83 7.25,9.58 -3.34,8 -8.94,14.28 -16.45,18.6 -8.12,4.7 -17.02,7.06 -25.06,7.75z" /> + <!-- Further (darker) hand --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_2" + android:fillType="evenOdd" + android:pathData="M415.42,367.63c3.53,7.75 3.7,14.84 -1.1,21.63 -1.47,2.08 -2.6,3.8 -4.82,0.34 -2.13,-3.3 -5.82,-5.17 -9.16,-7.13 -3.4,-2 -7.63,-0.44 -10.03,3.4 -3.4,5.4 -3.34,11.18 -0.97,16.85 1.1,2.62 0.66,3.87 -1.82,4.83 -3.94,1.53 -7.83,3.18 -11.77,4.7 -3.52,1.37 -4.58,-0.35 -3.97,-3.52 0.7,-3.68 1.24,-7.33 1.4,-11.1 0.43,-10.68 -0.85,-21.07 -3.92,-31.3 -1.53,-5.1 -0.83,-5.73 4.46,-5 1.3,0.2 2.6,0.54 3.87,0.44 4.14,-0.33 6.56,0.94 8.54,5.16 4.2,8.94 16.8,10.77 25.28,4.26 1.28,-1 2.43,-2.14 4.04,-3.57z" /> + <!-- Shadow on the body under the head --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd" + android:pathData="M235.36,329.56c-4.9,2.3 -9.54,-0.06 -14.25,-0.8 -6.7,-1.02 -13.33,-2.44 -19.7,-4.93 -12,-4.72 -13.4,-7.95 -8.3,-19.3 2.5,-2.93 6.32,-0.56 9.12,-2.3 1.8,3.48 5.23,4.16 8.6,4.97 4.9,8.3 12.62,13.74 20.15,19.33 1.43,1.06 2.93,2.03 4.4,3.03z" /> + <!-- Lil thing on the tail --> + <path + android:fillColor="#7F90A4" + android:fillType="evenOdd" + android:pathData="M29.1,436.15c0.18,-2.77 -0.3,-6.05 1.6,-8.98 1.46,-2.3 2.84,-3.05 4.6,-0.17 3.4,5.6 1.73,11.95 -4,15.13 -5.14,2.86 -4.4,7.85 -4.28,12.5 0.12,4.1 -2.63,8.4 -6.48,9.67 -3.95,1.3 -5.53,0.05 -5.24,-4.04 0.23,-3.2 1.85,-5.46 4.5,-7.1 1.9,-1.16 2.7,-2.1 0.24,-3.82 -1.63,-1.15 -2.64,-3.12 -4.28,-4.5 -2.75,-2.34 -2.9,-7.88 -0.46,-9.67 3.34,-2.43 6.07,-1.22 8.24,1.92 0.37,0.52 2.8,4.4 4.6,3.72 2.06,-0.77 0.83,-2.63 0.96,-4.67z" /> + <!-- Further (darker) eye whiteness--> + <path + android:fillColor="@color/elephant_friend_light_color_2" + android:fillType="evenOdd" + android:pathData="M352.32,197.8c-15.1,-5.94 -17.93,-25.7 -15.68,-37.5 1,-5.23 1.8,-10.65 5.4,-14.95 2.45,-2.9 5.87,-3.17 7.35,-0.58 0.72,1.27 1.88,3.03 -1.13,3.43 -2.77,0.37 -3.95,2.48 -4.73,4.95 -3.13,9.84 -1.9,26.38 2.35,32.03 1.93,2.58 3.1,4.33 6.02,7.17 2.7,2.62 2.47,2.93 0.4,5.46z" /> + <path + android:fillColor="@color/elephant_friend_light_color_1" + android:fillType="evenOdd" + android:pathData="M113.84,383.72c0.02,6.36 -0.75,6.95 -6.06,5.1 -2.14,-0.74 -4.16,-2.02 -6.35,-2.4 -4.78,-0.8 -3.77,-2.96 -1.96,-5.94 2.34,-3.86 5.7,-5.3 9.85,-4.28 4.17,1.03 4.24,4.8 4.52,7.52zM67.34,378.3c-2.27,-0.08 -0.84,-9.75 3.55,-11.8 4.3,-2 9.2,-0.32 11.33,3.1 1.98,3.2 1.1,11.72 -1.73,10.7 -2.27,-0.85 -7.85,-1.78 -13.16,-2zM393.28,396.27c0.18,-3.52 0.04,-7 3.72,-8.44 1.34,-0.52 8.55,7.1 8.54,8.64 -0.02,1.2 -8.14,7.03 -9.65,6.48 -3.38,-1.2 -1.88,-4.57 -2.62,-6.68zM137.77,397.7c-0.37,2.2 -1.43,4.63 -3.94,6.13 -0.57,0.34 -8.98,-6.25 -9,-6.88 -0.1,-2.22 5.47,-7.36 7.8,-7.23 2.6,0.14 5.22,3.88 5.14,7.98zM161.73,546.88c-2.15,-0.78 -6.73,1.67 -6.56,-3.93 0.13,-4.1 3.75,-7.33 7.26,-6.8 3.2,0.5 7.03,6.24 6.22,9.37 -0.4,1.56 -0.4,1.56 -6.92,1.36zM131.66,531.32c4.36,-0.1 8.77,6.83 6.85,10.74 -0.74,1.53 -1.95,0.9 -2.9,0.6 -2.02,-0.6 -3.97,-1.46 -5.94,-2.2 -2.3,-0.86 -5.52,-0.77 -4.6,-4.76 0.8,-3.6 3.42,-4.4 6.6,-4.38zM293.12,519.15c3.6,1.2 5.8,5.53 5.1,7.66 -0.8,2.48 -6.9,1.98 -8.64,1.9 -2.1,-0.13 -5.96,-1.66 -6.4,-3.37 -0.43,-1.6 0.95,-3.04 2.1,-4.16 1.38,-1.32 4.17,-3.24 7.84,-2.02zM320.45,517.26c2.46,0.8 5.4,1.4 5.67,4.47 0.25,3 -2.85,2.34 -4.63,3.04 -0.45,0.18 -0.92,0.35 -1.4,0.42 -1.84,0.28 -3.83,1.83 -5.5,0.35 -1.15,-1 -0.5,-2.84 0.3,-4.04 1.3,-2 2.78,-3.97 5.55,-4.24zM190.93,529.32c1.58,-0.08 2.62,0.53 2.2,2.25 -0.36,1.4 -0.93,2.77 -1.58,4.07 -1.38,2.76 -2.86,5.63 -6.32,6.02 -1.1,0.13 -0.9,-1.46 -0.83,-2.38 0.4,-4.83 3.67,-9.8 6.53,-9.96zM339.9,507.95c0.86,3.34 -0.14,4.58 -1.5,5.93 -1.1,-2.24 0.18,-3.46 1.5,-5.93z" /> + <!-- Dots inside the speech bubble --> + <path + android:fillColor="@color/elephant_friend_light_color_2" + android:fillType="evenOdd" + android:pathData="M449.67,120.45c-0.56,6.07 -4.03,9.7 -8.44,9.42 -4.9,-0.32 -7.8,-3.82 -7.27,-8.78 0.45,-4.3 5.1,-8.1 9.58,-7.8 4.93,0.3 6.04,3.64 6.13,7.15zM479.33,123.64c-0.57,6.07 -4.03,9.7 -8.44,9.42 -4.9,-0.32 -7.8,-3.82 -7.28,-8.78 0.45,-4.28 5.1,-8.08 9.58,-7.8 4.93,0.32 6.04,3.65 6.13,7.16zM416.6,116.9c-0.58,6.06 -4.04,9.7 -8.45,9.4 -4.9,-0.3 -7.8,-3.8 -7.27,-8.77 0.45,-4.28 5.1,-8.08 9.58,-7.8 4.93,0.32 6.03,3.65 6.13,7.16z" /> + <!-- Closer eye border, fur on the chest, trunk inners --> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M195.24,152.74c7.74,-11.83 18.38,-18.1 33.3,-15.86 16.5,2.5 27.95,10.5 30.4,25.3 3.8,22.83 -9.52,47.04 -37.8,49.3 -15,1.18 -27.8,-6.6 -33.2,-22.3 -5.08,-14.78 1.8,-28.04 7.3,-36.44zM233.13,336.23c-3.38,3.85 -4.73,8.13 -5.24,12.7 -0.2,1.73 -0.88,3.85 0.95,4.96 1.95,1.18 4.04,0.58 5.93,-0.66 2.84,-1.88 5.68,-3.77 8.6,-5.5 2.82,-1.66 4.3,-1.22 3.7,2.52 -1.02,6.47 0.4,12.64 2.83,18.6 2,4.88 6.94,5.66 10.4,1.6 2.55,-3 4.53,-6.36 6.14,-9.95 1.73,-3.87 4.12,-5.4 8.46,-3.1 4.98,2.6 6.83,1.5 8.35,-4.05 0.38,-1.4 0.65,-2.85 1,-4.27 0.34,-1.35 0,-3.3 1.95,-3.48 1.74,-0.15 3.12,1.1 3.77,2.64 2.08,4.84 1.66,9.83 -1.26,13.96 -2.9,4.1 -7.3,7 -12.64,5.4 -3.4,-1.03 -4.6,0.33 -6.08,2.78 -2.34,3.86 -4.54,8.03 -9.38,9.4 -9.73,2.77 -16.5,-1.07 -19.34,-10.9 -0.3,-1.1 -0.63,-2.2 -0.8,-3.33 -0.98,-6.03 -0.97,-6.04 -7.13,-4.43 -6.8,1.78 -11.85,-2.22 -11.2,-9.22 0.3,-3.02 1.43,-5.98 2.4,-8.9 0.54,-1.7 1.44,-3.3 2.33,-4.84 1.27,-2.2 3.04,-3.38 6.27,-1.93zM396.93,346.85c1.1,-8.24 4.9,-16.17 11.23,-22.87 1.35,-1.42 3.2,-2.32 5.2,-2.45 2.95,-0.2 4.2,1.35 3.64,4.25 -0.12,0.6 -0.54,1.17 -0.88,1.72 -3.8,6.24 -7.34,12.55 -6.02,20.32 0.07,0.45 -0.16,0.95 -0.25,1.43 -0.96,4.73 -3.7,7.37 -7.28,7 -3.46,-0.34 -5.76,-3.8 -5.64,-9.4z" /> + <!-- Stripes on the body --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd" + android:pathData="M339.6,394.63c-2.8,5.76 -5.05,12.45 -11.24,16.56 -1.6,1.04 -3.35,1.4 -4.9,-0.13 -1.4,-1.35 -1.2,-3.02 -0.4,-4.5 2.6,-4.88 5.2,-9.75 8,-14.5 1.23,-2.06 3.3,-3.22 5.84,-2.32 1.9,0.67 2.56,2.35 2.7,4.88zM99.37,195.3c0,5.1 -0.74,9.43 -1.8,13.7 -0.7,2.8 -2.47,4.76 -5.65,4 -3.27,-0.8 -3.62,-3.4 -2.94,-6.2 0.95,-3.96 1.85,-7.93 3.04,-11.82 0.76,-2.5 1,-6.24 4.72,-5.75 4,0.53 2.55,4.16 2.63,6.07zM306.3,404.28c-0.4,0.8 -0.8,2.3 -1.68,3.38 -2.87,3.52 -5.74,7.07 -8.97,10.24 -1.6,1.6 -4,3.56 -6.38,1.17 -2.1,-2.1 -0.64,-4.1 0.9,-5.97 3.17,-3.88 5.07,-8.74 9.14,-11.93 1.47,-1.13 3,-2.12 4.9,-1.1 1.4,0.76 2.08,2 2.1,4.2z" /> + <!-- Stripes on the trunk --> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M348.56,246.42c0.34,0.05 1.82,0.1 3.2,0.53 1.93,0.6 2.9,2.2 2.56,4.15 -0.4,2.17 -2.1,2.8 -4.1,2.32 -5,-1.2 -9.1,1.4 -13.42,3.06 -1.2,0.47 -2.28,1.25 -3.46,1.75 -1.94,0.83 -3.96,1 -5.14,-1.1 -1.27,-2.24 0.28,-3.54 2.07,-4.67 5.27,-3.32 10.78,-5.92 18.3,-6.04z" /> + <!-- Some stripes on the body --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd" + android:pathData="M93.92,356.84c0.62,-4.05 2.12,-7.92 4.52,-11.26 2.05,-2.86 13.07,-8.75 11,-0.02 -0.9,3.88 -13.95,23.17 -15.52,11.28 0.87,-5.7 0.2,1.54 0,0zM293.28,451.9c-2.74,-0.2 -4.53,-0.6 -5.22,-2.56 -0.7,-1.95 0.73,-2.94 2.15,-3.84 4.1,-2.6 7.46,-6.24 11.86,-8.45 2.12,-1.06 3.98,-1.3 5.73,0.54 1.98,2.07 0.75,3.87 -0.58,5.6 -3.77,4.85 -9.4,6.57 -13.94,8.7zM157.6,506.77c-0.1,0.93 -0.1,2.24 -0.4,3.5 -0.5,2.28 -1.95,3.8 -4.36,3.85 -2.33,0.07 -3.6,-1.68 -4,-3.62 -1.13,-5.65 0,-11.33 1.05,-16.8 0.6,-3.12 3.28,-2.43 4.95,0.03 2.62,3.85 2.18,8.37 2.76,13.04zM121.98,171.47c0,4.06 -1.3,8 -1.03,12.08 0.15,2.4 -1.83,3.8 -4.16,3.9 -2.48,0.1 -3.32,-1.92 -3.55,-3.9 -0.8,-6.62 1.78,-12.7 3.4,-18.92 0.26,-0.94 1.18,-1.55 2.28,-1.5 1.32,0.1 2.18,0.96 2.38,2.1 0.4,2.05 0.47,4.16 0.68,6.24zM127.7,234.96c-0.1,1.63 -0.28,4.04 -0.38,6.46 -0.1,2.66 -1.78,3.8 -4.1,3.78 -2.47,-0.03 -4.1,-1.8 -3.97,-4.06 0.28,-4.8 0.3,-9.73 3.2,-13.94 0.83,-1.2 1.7,-2.65 3.37,-2.32 2.14,0.43 1.78,2.4 1.85,3.96 0.1,1.77 0.02,3.55 0.02,6.12zM177.7,488.12c-0.52,3.22 0.87,8.16 -4.78,7.6 -5.6,-0.57 -4,-5.25 -3.75,-8.8 0.15,-2.08 0.56,-4.17 1.07,-6.2 0.4,-1.62 1.35,-3 3.28,-3.08 1.93,-0.07 2.9,1.24 3.5,2.83 0.92,2.47 1.26,5 0.7,7.65zM272.2,445.54c-0.3,0.63 -0.48,1.5 -1,2.03 -4.33,4.35 -10.06,5.68 -15.74,6.93 -1.43,0.32 -2.77,-0.9 -3.1,-2.53 -0.34,-1.53 0.66,-2.5 1.86,-3.07 4.22,-2.06 8.47,-3.98 12.57,-6.3 2.18,-1.24 4.85,-0.65 5.4,2.94zM102.32,145.58c1.64,-5.07 4.9,-9.88 8.67,-14.37 0.64,-0.77 1.66,-1 2.65,-0.48 1,0.53 1.38,1.55 1.1,2.5 -1.63,5.42 -3.5,10.8 -6.2,15.78 -0.85,1.57 -2.84,1.46 -4.45,0.88 -1.67,-0.62 -1.83,-2.1 -1.78,-4.3z" /> + <!-- Stripes --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd" + android:pathData="M144.84,486.52c0,1.3 0.1,2.94 -0.03,4.55 -0.17,2.34 -1.25,4.17 -3.8,4.27 -2.65,0.1 -4.1,-2 -4,-4.07 0.23,-4.34 0,-8.8 1.37,-13.02 0.5,-1.5 1.36,-2.67 3.15,-2.47 1.52,0.17 2.25,1.36 2.45,2.68 0.4,2.56 0.57,5.15 0.87,8.06z" /> + <path + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd" + android:pathData="M81.02,341.44c1.3,-3.72 4.4,-17.26 11.34,-12.86 7.63,4.85 -12.85,22.17 -11.34,12.87 1.3,-3.7 -0.2,1.3 0,0zM107.94,313.03c0.88,-3.87 1.65,-7.9 4.5,-10.98 1.52,-1.63 3.36,-2.55 5.6,-1.08 2.35,1.54 1.9,3.58 0.87,5.53 -1.56,2.98 -3.24,5.9 -5.06,8.75 -0.9,1.44 -2.4,2.25 -4.18,1.52 -1.6,-0.66 -1.77,-2.14 -1.72,-3.74z" /> + <!-- Some dark stripes --> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M342.43,271.95c-1.83,0 -3.2,0 -3.8,-1.48 -0.53,-1.25 0.2,-2.2 1.04,-3 4.12,-3.8 9.28,-5.16 14.6,-5.7 2.03,-0.2 4.25,0.87 4.33,3.64 0.06,2.56 -1.9,2.55 -3.7,2.87 -4.44,0.8 -8.7,2.26 -12.47,3.68zM202.22,302.22c-1.4,7.32 -5.82,2.58 -9.13,2.3 -2.34,-1.72 -4.7,-3.4 -6.98,-5.2 -1.35,-1.07 -2.27,-2.5 -1.12,-4.2 1,-1.46 2.65,-1.47 3.9,-0.6 4.2,2.98 9.44,4.16 13.32,7.7z" /> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M356.13,286.15c-2.13,-0.24 -5.3,0.83 -5.03,-2.32 0.3,-3.4 3.6,-4.4 6.62,-4.34 2.07,0.03 4.6,0.8 4.2,3.6 -0.5,3.4 -3.13,3.7 -5.8,3.05z" /> + <!-- Closer (brighter) eye whiteness--> + <path + android:fillColor="@color/elephant_friend_light_color_2" + android:fillType="evenOdd" + android:pathData="M249.37,187.45c-7.92,14.6 -25.8,23.77 -44.07,13.36 -8.45,-4.8 -11.9,-14.85 -11.95,-23.45 -0.1,-12.7 6.15,-22.22 16.36,-30.07 10.78,-8.3 34.38,-4.27 40.84,10.76 2.74,6.4 3.9,20.05 -1.17,29.4z" /> + <!-- Closer eye pupil --> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M245.26,175.92c0.12,9.8 -6.64,19.24 -18.34,21.65 -12.87,2.66 -23.04,-5.27 -25.9,-15.54 -2.43,-8.64 3.8,-20.3 9.64,-25.97 6.9,-6.73 18.22,-7.67 25.9,-2.63 8.8,5.8 8.6,14.37 8.7,22.5z" /> +</vector> diff --git a/app/src/main/res/drawable/errorphant_error.xml b/app/src/main/res/drawable/errorphant_error.xml new file mode 100644 index 0000000..1c7948a --- /dev/null +++ b/app/src/main/res/drawable/errorphant_error.xml @@ -0,0 +1,129 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="200dp" + android:height="153dp" + android:viewportWidth="600" + android:viewportHeight="460"> + <!-- Border --> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M321.28,457.4c-12.3,0 -37.47,-6.95 -60.17,-29.47 -12.6,-12.53 -26.3,-21.46 -43.42,-27.73 -2.63,-0.96 -9.86,-2.85 -17.5,-4.86 -11.66,-3.05 -24.87,-6.5 -31.38,-9.04 -13.04,-5.1 -28.76,-14 -43.14,-24.46 -1.02,-0.75 -1.57,-1.06 -2.3,-1.1 -1.82,-0.04 -3.68,-0.07 -5.53,-0.07 -8.96,0 -18.5,0.65 -29.18,2 -6.8,0.84 -34.75,3.5 -41.1,2.12 -11,-2.4 -22.87,-3.7 -32.52,-15.17 -7.64,-9.08 -11.12,-19.6 -10.63,-32.18 0.36,-9.35 1.7,-26.2 6.68,-34.05l0.67,-1.06c4.8,-7.63 13.73,-21.84 28.02,-21.84 0.34,0 0.68,0 1.03,0.02 1.02,0.05 2.26,0.08 3.7,0.08 7.73,0 20.05,-0.74 30.14,-2.38 16.3,-2.65 17.26,-5.95 17.26,-5.98l0.1,-1.63c0.58,-8.3 1.43,-20.6 6.28,-28.1 -0.83,-0.32 -1.8,-0.77 -2.73,-1.5 -12.48,-9.97 -15.76,-20.93 -16.84,-34.32 -0.48,-6.1 0.05,-11.78 1.64,-17.4l0.03,-0.13c0.43,-1.54 0.53,-1.67 -0.14,-2.06 -9.97,-5.8 -15.37,-16.72 -5.3,-23.35 0.6,-0.4 0.3,-1.64 0.34,-2.7 0.02,-0.6 0.05,-1.2 0.1,-1.8 0.76,-7.43 3.88,-12.48 9.54,-15.44 2.66,-1.37 5.35,-2.07 8,-2.07 6.03,0 10.87,3.63 12.96,9.7 1.16,3.37 2.34,3.94 4.54,3.94 0.4,0 0.84,-0.03 1.3,-0.08 1.13,-0.13 2.13,-0.18 3.05,-0.18 4.54,0 7.57,2.4 9.9,5.64 3.43,4.77 5.56,10.2 0.66,17.1 -4,5.7 -10.02,8.3 -17.02,10.24 -1.23,0.34 -1.5,0.47 -1.58,2.15v0.12c-0.37,7.24 0.26,17.12 5.68,22.78 1.35,1.42 1.88,2.15 2.22,2.38 0.28,-0.26 0.84,-0.9 1.7,-2.43 8.93,-15.66 22.3,-37.78 35.42,-49.08 28.43,-25.4 76.36,-37 108.83,-37 24.22,0 52.2,6.3 76.8,17.3 9.6,4.3 17.9,8.76 25.42,13.65 1.9,1.25 4.13,1.98 6.44,2.7 8.9,2.7 13.9,10.18 12.82,19.1 3.34,-0.78 7.3,-1.6 11.3,-1.6 4.18,0 7.88,0.87 11.3,2.62 3.53,1.8 7.63,4.6 8.15,10.23 0.05,0.52 0.13,0.83 0.18,1 0.04,0 0.1,0 0.15,0 0.43,0 1.05,-0.1 1.9,-0.3 12.7,-2.87 25.33,-4.34 37.5,-4.34 11.6,0 23.18,1.32 34.4,3.93 10.1,2.34 18.22,5.1 25.6,8.7 6.36,3.1 6.6,7.85 6.18,10.4 -0.48,2.88 0.4,4.22 4.32,6.5 5.64,3.3 9.18,7.57 10.23,12.33 0.98,4.45 -0.2,9.25 -3.45,13.9 -1.18,1.68 -2.66,3.52 -4.7,4.4 -0.87,0.36 -1.27,0.66 -1.45,0.8 0.1,0.24 0.34,0.76 1.07,1.65 1.67,2.07 2.87,4.47 3.8,6.54 3.88,8.5 1.5,17.12 -6.2,22.5 -0.55,0.4 -1.12,0.78 -1.7,1.15 19.83,4.68 37.5,11.4 53.88,20.43 33.24,18.4 22.08,59.78 14.97,74.18 -4.9,9.94 -14.1,17.58 -24.62,20.45 -0.02,0.2 -0.02,0.58 0.05,1.3 1.67,15.82 -3.7,29.94 -15.95,41.95 -3.16,3.1 -3.52,5.05 -1.54,8.36 2.03,3.4 2.47,7.07 1.24,10.63 -1.87,5.4 -7.05,9.1 -10.92,10.6 -3.6,1.4 -7.32,2.1 -11.08,2.1 -4.3,0 -8.7,-0.92 -13.12,-2.72 -6.73,-2.75 -9.85,-8.4 -12.6,-13.36l-0.4,-0.7c-2.64,-4.76 -5.9,-7.54 -10.58,-9 -7.04,-2.23 -10.63,-6.94 -9.85,-12.94 0.77,-6.1 6.87,-11.87 13.3,-12.58 0.25,-0.03 0.5,-0.04 0.78,-0.04 0.64,0 1.28,0.08 1.83,0.15 0.33,0.05 0.64,0.08 0.9,0.1 0,-0.05 0,-0.1 0,-0.17 0.42,-4.64 -1.65,-6.03 -3.24,-6.67 -1.93,-0.78 -3.86,-1.57 -5.8,-2.36 -7.1,-2.92 -14.45,-5.94 -21.82,-8.17 -4.15,-1.24 -9.04,-4.14 -13.1,-1.85 -27.47,15.4 -63.35,20.64 -83.45,12.53 2.3,12.2 -0.4,25.7 -4,34.87 -5.3,13.6 -20.65,28.48 -37.1,29.84 -2.2,0.2 -4.6,0.3 -7.12,0.3z" /> + <!-- Speech bubble background --> + <path + android:fillColor="@color/elephant_friend_accent_color" + android:fillType="evenOdd" + android:pathData="M391.52,111.36c-8.73,-4.53 -16.67,-10.5 -21.26,-19.1 -19.5,-36.4 2.8,-73.47 32.2,-84.1 23,-8.3 47.55,-4.8 64.37,6.84 33.84,23.44 34,68.18 7.96,92.03 -16.08,14.72 -38.5,13 -45.05,14.36 -3.35,0.68 -22.67,15.33 -28.36,21.22 -1.4,1.44 -2.97,2.76 -5.16,1.85 -2.32,-0.96 -2,-3.1 -1.98,-5.04 0,-2.1 -0.28,-26.8 -2.74,-28.07z" /> + <!-- Lighter body part --> + <path + android:fillColor="@color/elephant_friend_body_color" + android:fillType="evenOdd" + android:pathData="M69.6,356.3c-5.73,-8.7 -19.5,-8.76 -24.82,0.05 -1.18,1.95 -2.48,1.28 -3.7,1 -2.07,-0.45 -4.18,-1.1 -4.1,-3.97 0.18,-5.66 -2.76,-9.95 -6.65,-13.6 -3.22,-3 -7.08,-4.68 -11.7,-3.47 -5.06,1.34 -4.73,-3.3 -6.2,-5.74 -1.56,-2.58 0.45,-3.45 2.56,-4.43 12.08,-5.62 13.63,-18.92 3.07,-27.3 -1.8,-1.43 -0.9,-2.26 -0.8,-4 0.84,-11.47 15,-28.07 21.86,-26.92 12.13,2.03 61.1,0 61.1,-12.66 0,-18.32 4.07,-24.24 9.4,-35.55 0.95,-2.02 1.7,-3.74 -1.2,-4.5 -12.15,-3.25 -18.16,-12.13 -21.06,-23.35 -2.28,-8.87 -3.1,-17.97 0.77,-26.77 0.98,-2.22 2.08,-4.6 4.92,-4.93 3.77,-0.44 5.73,2.27 4.84,6.97 -1.96,10.3 -0.7,20.06 4.67,29.18 3.1,5.27 9.45,8.6 13.92,7.7 1.6,-0.33 2.36,-0.8 3.17,-2.33 10.38,-19.3 19,-37.94 39.08,-53.72 37.87,-29.77 70.44,-32.55 97.17,-32.55 45.35,0 87.96,17.5 95.15,25.83 -11.14,3.8 -20.23,10.02 -28.45,17.45 -5.96,5.4 -10.9,11.7 -14.47,18.9 -1.56,3.16 -3.62,5 -7.27,5.66 -11.28,2 -22.37,4.95 -33.08,8.95 -17.87,6.68 -33.15,17.48 -46.26,31.17 -13.46,14.05 -22.82,30.32 -25.44,49.9 -1,7.45 0.6,10.16 7.9,12.46 1.8,0.57 1.86,1.4 1.58,2.82 -0.95,4.82 -0.8,9.7 -0.7,14.57 0.14,9.46 8.3,14.97 17.36,11.86 5.43,-1.86 9.5,-5.5 13,-9.9 7.64,9.83 20.6,9.16 29.07,-1.14 6.94,-8.44 12.32,-17.56 14.23,-28.47 0.42,-2.38 1.03,-4.68 -1.6,-5.95 -3.07,-1.5 -3.4,1.5 -4.43,3.12 -3.75,5.8 -5.07,12.7 -8.37,18.7 -3.42,6.22 -7.25,12.06 -14.84,13.94 -3.7,0.93 -6.4,0.08 -7.66,-3.9 -0.65,-2.1 -1.1,-5.13 -3.9,-4.96 -3.18,0.2 -6.4,1.37 -8.22,4.47 -1.87,3.2 -4.5,5.57 -7.93,6.92 -4.45,1.75 -7.74,0.02 -9,-4.6 -1.46,-5.37 -0.34,-10.67 0.66,-15.94 1.2,-6.37 0.74,-7.37 -5.55,-7.6 -3.83,-0.16 -4.8,-1.94 -5.3,-5.42 -1.4,-10.06 3.9,-17.97 8.25,-26.03 6.46,-12 15.9,-21.5 26.28,-30.5 15.37,-13.35 33.9,-18.88 52.62,-24.67 5.7,-1.77 11.74,-1.94 17.27,-4.9 3.57,-1.9 5.42,-4.7 7.13,-7.58 9.46,-16.04 22.5,-27.63 40.53,-33.23 1.98,-0.6 4.2,-0.7 6.3,-0.62 9.52,0.32 13.1,6.6 8.56,14.92 -0.78,1.43 -1.86,2.73 -2.4,4.22 -0.5,1.4 -2.26,2.88 -0.67,4.44 1.55,1.54 3.14,0.08 4.48,-0.57 5.24,-2.58 10.84,-3.38 16.56,-3.83 4.2,-0.34 7.74,1.32 11,3.55 3.6,2.45 3.12,6.53 -0.94,8.3 -1.66,0.73 -4.04,1.1 -3.3,3.34 0.86,2.65 3.22,1.34 5.02,0.77 5.85,-1.86 11.02,-0.03 15.7,3.2 2.9,2 3.03,4.66 -0.3,6.57 -2.55,1.46 -5.96,2.36 -4.6,6.23 1.3,3.7 4.16,6.8 8.15,6.97 13.5,0.57 23.78,7.23 32.93,16.24 2.63,2.58 5.95,4.48 8.23,8 -2.18,1.1 -4.04,0.43 -5.88,0.13 -12.5,-2 -23.3,1.74 -32.6,9.88 -3.86,3.35 -5.5,8.1 -6.13,12.98 -0.85,6.53 4.4,13.76 11.48,14.03 11.92,0.44 23.68,2.8 35.62,2.6 4.62,-0.06 8.8,-2.13 13.5,-3.94 1.5,12.74 6.87,23.74 12.65,34.73 8.35,15.86 20.1,28.36 36.77,34.92 22.44,8.85 34.3,40.07 25.13,60.53 -3.08,6.86 -8.52,12.25 -12.52,18.57 -2.76,4.38 -4.1,8.6 -1.64,13.25 2.26,4.3 -0.05,6.2 -3.6,7.08 -13,3.2 -20.02,0.87 -27.64,-11.32 -2.52,-4.02 -5.66,-7.08 -10.42,-8.2 -2.42,-0.58 -5.72,-1.6 -5.24,-4.06 0.65,-3.37 4.3,-5.7 7.6,-4.62 5.52,1.84 7.5,-0.47 8.54,-5.14 0.2,-0.26 0.3,-0.55 0.4,-0.87 0.63,-2.7 4.9,-3.34 4.16,-6.7 -1.03,-4.7 -3.6,-8.73 -6.6,-12.4 -5.77,-7 -12.74,-12.52 -21.36,-15.84 -5.74,-2.2 -11.82,-2.4 -17.78,-3.28 -2.42,-0.35 -4.73,-0.5 -4.2,-3.87 3.85,-7.1 6.87,-14.28 5.16,-22.7 -0.87,-4.27 -3.37,-5.35 -7.07,-4.56 -3.67,0.76 -7.3,1.8 -10.87,2.96 -17.76,5.8 -35.05,2.7 -51.7,-3.8 -5.15,-2 -10.2,-6 -11,-13.4 -0.7,-6.38 -5.26,-11.4 -10.52,-15.3 -1.34,-1 -2.92,-1.7 -4.46,-0.27 -1.58,1.43 -0.7,2.9 0.04,4.4 1.42,2.86 3.93,5.13 4.96,8.7 -4.48,0.26 -8.5,-0.5 -12.52,0.23 -13.87,2.54 -26.6,15.42 -20.08,32.27 0.36,0.94 1.87,1.92 0.46,2.94 -1.14,0.82 -2,-0.07 -2.88,-0.98 -6.13,-6.43 -9.6,-11.9 -17.46,-16.82 -1.64,-1.02 -0.26,-1.8 0.6,-2.56 9.15,-8.4 10.2,-15.07 4.03,-25.73 -3.64,-6.28 -2.94,-8.92 2.85,-13.65 6.3,-5.16 13.24,-8.12 21.55,-7.82 6.38,0.23 12.33,-1.2 17.42,-5.36 5.8,-4.77 8,-16.44 3.17,-21.03 -3.6,-3.44 -7.15,-7.06 -11.5,-9.74 -9.28,-5.7 -19.2,-4.6 -29,-2.65 -13.74,2.77 -25.8,8.4 -32.57,21.72 -4.28,8.42 -4.47,16.48 1.75,24.1 0.93,1.16 2.05,2.2 1.02,3.86 -4.24,6.82 -4.57,14.5 -4.62,22.14 -0.02,3.27 -1.04,3.68 -3.64,2.3 -6.06,-3.16 -12.72,-4.06 -19.38,-4.92 -1.76,-0.22 -4.1,-0.43 -4.64,1.66 -0.58,2.35 1.94,2.72 3.5,3.17 11.46,3.24 21.72,8.8 31.45,15.47 3.1,2.12 6.43,3.88 9.97,5.25 1.65,0.64 3.23,1.58 4.66,2.63 1.2,0.87 2.33,2.2 1.13,3.72 -1.3,1.62 -2.2,0.02 -3.2,-0.72 -3.6,-2.66 -7.78,-3.45 -12.08,-3 -8.3,0.9 -12.36,7.23 -9.82,15.53 1.2,3.92 1.26,5.22 -3.6,4.1 -7.13,-1.62 -12.6,1.03 -15.35,6.45 -3.26,6.48 -3.14,12.7 2.5,16.13 5.05,3.06 1.16,9.17 0.1,13.5 -1.7,6.83 -3.96,13.37 -1.1,21.64 3.06,8.8 13.08,19.8 12.14,20.82 0,0 -8.6,-7.64 -15.87,-14.15 -7.17,-6.43 -21,-18.34 -29.36,-23.58 -9.32,-5.86 -28.5,-12.07 -43.38,-13.65 -13.67,-1.44 -22.47,-4.1 -37.7,-10.97 -11.66,-5.25 -28.63,-19.36 -31.93,-20.23 -7.05,-1.87 -10.82,-2.45 -14.3,-2.45 -8.63,0 -34.66,5.76 -42.13,3.82 -1.6,-0.02 -3.2,0.58 -4.56,-0.75zM420.93,197.16c-0.24,-0.84 0.67,-1.47 1.2,-2.22 3.77,-5.33 3.78,-8.13 0,-13.15 -2.8,-3.74 -2.47,-4.4 1.98,-5.17 17.1,-2.97 42.73,-3.3 67.23,3.2 5.07,0.85 9.6,3.06 13.97,5.7 2.63,1.6 2.98,2.93 0.36,5.08 -3.75,3.1 -3.55,4.57 0.77,7.15 4.4,2.62 8.2,5.94 11.78,9.5 5.1,5.04 4.2,9.04 -2.6,11.7 -0.93,0.36 -1.9,0.6 -2.76,1.05 -4.6,2.4 -5.3,6.5 -1.78,10.25 2.34,2.5 4.82,4.87 6.92,7.56 3.08,3.95 2.6,6.92 -1.04,10.2 -4.1,3.67 -8.33,5.7 -14.24,3.87 -4.27,-1.3 -9.3,0.06 -13.65,0.58 -5.76,0.68 -6.3,-3.54 -10.32,-11.05 -6.53,-12.22 -16.73,-22.23 -28.2,-30.65 -8.96,-6.58 -18.9,-10.57 -29.6,-13.6z" /> + <!-- Closer (lighter) tusk --> + <path + android:fillColor="@color/elephant_friend_light_color_2" + android:fillType="evenOdd" + android:pathData="M367,365c-5.62,-3.22 -12.82,-6.04 -18.2,-13.6 -2.02,-2.8 -2.67,-11.16 -2.17,-14.35 1.73,-11.04 12.7,-18.45 23.8,-16.7 3.8,0.58 6.42,2.86 8.56,5.17 10.5,11.33 24.06,15.2 38.78,16.66 12.5,1.24 24.8,0.75 36.6,-4.23 4.3,-1.8 4.3,1.12 3.97,3.94 -1.14,9.45 -7.24,15.76 -14.25,21.3 -9.04,7.13 -17.35,11.42 -30.92,12.56 -12.92,1.08 -34.5,-4.03 -46.19,-10.75z" /> + <!-- Further (darker) tusk --> + <path + android:fillColor="@color/elephant_friend_light_color_1" + android:fillType="evenOdd" + android:pathData="M489.05,286.78c8.53,2.93 16.83,5.34 25.73,5.84 11.46,0.65 22.5,-0.88 33.27,-4.5 3.55,-1.2 3.46,0.85 2.86,2.74 -3.4,10.74 -9.5,19.5 -19.88,24.7 -2.45,1.24 -5.13,2.04 -7.76,2.88 -8.5,2.74 -14.32,-0.93 -19.8,-7.23 -6.36,-7.3 -10.17,-15.83 -14.4,-24.42z" /> + <!-- Further (darker) leg part--> + <path + android:fillColor="@color/elephant_friend_dark_body_color_2" + android:fillType="evenOdd" + android:pathData="M521.15,285.08c-10.62,-0.05 -21.34,-1.6 -30.67,-7.75 -2.95,-1.94 -7.1,-12.07 -6.4,-15.37 0.5,-2.35 2.38,-1.63 3.68,-1.47 5.45,0.68 10.94,1.24 16.28,2.45 12.03,2.73 24.32,4.6 35.73,9.54 3.12,1.35 8.28,2.13 8.1,5.6 -0.18,3.48 -12.63,7.05 -26.72,6.98z" /> + <!-- Closer eye whiteness --> + <path + android:fillColor="@color/elephant_friend_light_color_2" + android:fillType="evenOdd" + android:pathData="M307.15,290.35c-2.16,-1 -11.03,-13.56 -3.25,-23.94 11.47,-15.3 24.62,-23.65 47.68,-17.6 4.18,1.1 10.06,5.07 12.45,8.46 4.63,6.57 1.92,14.6 -5.87,16.43 -5.2,1.22 -13.14,1.58 -20.02,3.45 -6.97,1.9 -15.18,11.88 -23.14,13.56 -2.8,4.1 -5.34,2.1 -7.85,-0.35z" /> + <!-- Lil' thing on the tail--> + <path + android:fillColor="@color/elephant_friend_dark_body_color_2" + android:fillType="evenOdd" + android:pathData="M78.46,151.37c0.96,-5.43 7,-1.63 5.7,-7.1 -1.1,-4.74 -0.97,-9.45 2.4,-13.4 2.78,-3.26 5.3,-3.13 8.2,0.04 2.6,2.85 2.92,6.35 3.3,9.96 0.63,5.8 0.8,5.77 6.05,3.27 5.02,-2.37 10.07,-1.92 13.32,1.04 2.16,1.97 2.72,4.04 0.8,6.38 -2.4,2.87 -4.7,5.77 -8.53,7.02 -3.03,1 -5.3,1.66 -8.2,-1.2 -4.23,-4.17 -10,-3.9 -14.03,0.2 -2.54,2.57 -9.7,-2.3 -9,-6.2z" /> + <!-- Eyedrop fill --> + <path + android:fillColor="@color/elephant_friend_accent_color" + android:fillType="evenOdd" + android:pathData="M307.15,290.35c3.14,-1.97 5.2,0.92 7.85,0.36 -0.26,7.52 1.08,14.38 6.46,20.28 3.2,3.52 0.97,9.78 -3.76,12.32 -4.8,2.58 -11.38,0.7 -13.53,-3.92 -1.57,-3.36 -1.66,-6.9 -0.92,-10.48 1.28,-6.18 2.6,-12.36 3.9,-18.55z" /> + <!-- Small shadow at the bottom of the tusk --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_2" + android:fillType="evenOdd" + android:pathData="M459.46,357.55c4.5,2.6 9.65,1.97 14.47,2.82 18.8,3.3 32.48,13.36 37.03,31.44 2.25,8.94 -6.36,13.13 -9.36,14.08 3.07,-4.7 3.75,-8.56 3.8,-10.17 0.3,-6.3 -1.26,-11.65 -6.9,-15.6 -7,-4.9 -14.54,-8.7 -22.1,-12.47 -3.74,-1.86 -8.1,-2.38 -12.38,-2.3 -3.13,0.07 -6.15,-0.4 -9.94,-1.33 1.97,-2.36 3.67,-4.4 5.38,-6.45z" /> + <!-- Further eye whiteness--> + <path + android:fillColor="@color/elephant_friend_light_color_2" + android:fillType="evenOdd" + android:pathData="M450.8,260.04c-7,0 -14,0.26 -20.97,-0.07 -8.43,-0.4 -10.94,-5.5 -6.68,-12.84 5.14,-8.82 13.86,-10.52 22.96,-11.36 6.47,-0.6 12.64,0.72 18.66,3.02 5.74,2.18 8.25,6.55 8.57,12.36 0.3,5.44 -2.8,8.83 -8.36,9.04 -4.72,0.18 -14.2,-0.1 -14.2,-0.16z" /> + <!-- Further leg part--> + <path + android:fillColor="@color/elephant_friend_dark_body_color_2" + android:fillType="evenOdd" + android:pathData="M577.68,291.95c-7.1,-0.48 -14.1,3.47 -22.36,11.36 0.7,-4.76 3.92,-13.55 4.77,-16 0.7,-2.1 4.4,-3.03 7,-2.6 3.9,0.67 8.15,3.78 10.58,7.25z" /> + <!-- Fingers --> + <path + android:fillColor="@color/elephant_friend_light_color_1" + android:fillType="evenOdd" + android:pathData="M283.18,382.77c-4.04,-3.35 -6.63,-6.3 -4.78,-11.25 1.56,-4.2 3.9,-7.02 8.8,-6.82 1.83,0.07 3.7,0.16 1.85,2.62 -3.32,4.4 -4.53,9.57 -5.87,15.45zM18.7,311.07c0.16,3.76 -1.36,6.56 -4.7,8.4 -2.17,1.17 -3,0.52 -2.7,-1.86 0.38,-3.2 0.67,-6.4 1.08,-9.6 0.24,-1.9 0.67,-4.34 2.9,-4.2 1.97,0.1 2.92,2.3 3.15,4.38 0.1,0.96 0.2,1.93 0.28,2.9zM312.4,347.97c-5.32,2.54 -10.6,3.83 -14.64,9.04 -0.3,-4.72 -0.6,-8.22 2.9,-10.15 3.73,-2.06 7.6,-1.9 11.73,1.12zM28.94,351c-3.7,-2.66 -8.33,-4.16 -10.83,-8.58 5.63,-1 8.32,0.55 11.04,6.08 0.46,0.87 0.5,1.7 -0.2,2.5zM62.3,357.75c-4.44,0.04 -8.86,2.5 -13.96,0.37 4.93,-3.02 9.36,-3.38 13.95,-0.37z" /> + <!-- Trunk inner part--> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M510.95,411.05c4.92,-0.04 9.25,1.2 13.14,3.8 3.2,2.16 5,4.66 3.7,9 -1.37,4.6 -4.58,5.8 -8.68,6.46 -4.23,0.7 -5.76,-1.95 -7.2,-5.05 -1,-2.2 -1.9,-4.44 -3.94,-6.02 -1.9,-1.48 -5.42,-2.27 -3.96,-5.7 1.3,-3.02 4.25,-3.1 6.93,-2.48z" /> + <!-- Stripe on the trunk --> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M474.3,297.4c0.12,2 -1.23,2.96 -2.84,3.43 -6.44,1.83 -12.63,4.12 -18.05,8.18 -1.06,0.8 -2.42,1.15 -3.56,0.05 -1.18,-1.14 -0.5,-2.37 0.1,-3.55 2.76,-5.57 14.32,-12.06 20.4,-11.5 2.2,0.2 3.75,1 3.97,3.4z" /> + <!-- Body stripes --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd" + android:pathData="M253.55,243.95c-2.75,-0.1 -4.37,-2.87 -2.58,-5.24 3.1,-4.12 6.53,-8.05 10.2,-11.68 1.88,-1.87 4.58,-1.78 6.5,0.44 1.78,2.03 0.78,3.93 -0.82,5.56 -2.83,2.9 -5.7,5.77 -8.6,8.6 -1.3,1.24 -2.8,2.23 -4.7,2.33zM257.65,136c2.96,-0.06 5,0.52 5.52,3.06 0.5,2.38 -1.25,3.76 -3.1,4.36 -5.13,1.67 -10.58,1.56 -15.9,2.1 -1.67,0.16 -3.15,-1.03 -3.36,-2.94 -0.2,-1.92 1.18,-2.98 2.68,-3.7 4.68,-2.2 9.87,-1.93 14.17,-2.87zM68.42,307c-2.6,-0.35 -5.36,-0.35 -5.72,-3.16 -0.35,-2.7 2.35,-3.17 4.3,-3.75 3.9,-1.16 7.84,-2.1 11.75,-3.14 2.14,-0.57 4,-0.27 4.74,2.05 0.7,2.15 -0.17,3.84 -2.2,4.82 -4.16,2 -8.74,2.3 -12.88,3.18zM87.55,284.7c-1.2,-0.3 -2.46,-0.55 -3.7,-0.94 -1.12,-0.35 -2.1,-0.98 -2.2,-2.3 -0.07,-1.33 0.8,-2.1 1.93,-2.48 4.6,-1.6 9.15,-3.32 13.8,-4.68 2.14,-0.62 4.45,-0.1 5.14,2.42 0.68,2.5 -0.92,3.97 -3.17,4.65 -3.87,1.17 -7.78,2.2 -11.8,3.34z" /> + <path + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd" + android:pathData="M46.94,291.37c-2.74,-0.76 -8.07,1.96 -8.54,-2.92 -0.42,-4.35 5,-3.4 8.04,-4.2 2.48,-0.67 5.13,-0.67 7.7,-1 2.02,-0.23 3.6,0.28 4.05,2.47 0.4,2 -0.4,3.4 -2.24,4.34 -2.73,1.4 -5.7,0.95 -9.02,1.3z" /> + <path + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd" + android:pathData="M188.7,176.74c-1.22,0.04 -2.3,-0.24 -2.84,-1.44 -0.58,-1.34 -0.12,-2.68 0.8,-3.56 3.83,-3.6 8.55,-5.18 13.7,-5.2 2,0 3.14,1.56 3.38,3.54 0.24,2.06 -0.77,3.54 -2.7,4 -4.1,1 -8.23,1.8 -12.34,2.66zM195.55,155.9c-2.3,0.25 -3.38,-0.06 -3.9,-1.26 -0.6,-1.34 -0.28,-2.64 0.9,-3.44 3.47,-2.38 7,-4.75 11.03,-6.07 2.16,-0.7 4.35,-0.52 5.26,2 0.82,2.28 -0.57,3.8 -2.5,4.6 -3.86,1.64 -7.83,3.05 -10.8,4.2z" /> + <!-- Trunk stripe --> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M478.2,305.85c2.28,0.06 4.03,0.75 4.48,3.1 0.38,2 -0.9,2.83 -2.57,3.55 -2.68,1.16 -5.23,2.65 -7.95,3.76 -1.7,0.7 -3.67,0.9 -4.87,-0.92 -1.2,-1.8 0.3,-3 1.4,-4.18 2.63,-2.8 5.65,-4.92 9.52,-5.3z" /> + <!-- Body stripe --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd" + android:pathData="M174,166.22c-0.37,0.7 -0.53,1.42 -0.98,1.77 -3.33,2.54 -6.66,5.08 -10.12,7.46 -1.1,0.76 -2.56,0.66 -3.52,-0.52 -0.8,-0.96 -0.84,-2.24 -0.17,-3.2 2.43,-3.44 5.12,-6.73 9.32,-8.1 2.4,-0.8 4.43,-0.03 5.47,2.58z" /> + <!-- Trunk stripes--> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M481.95,327.98c-1.52,0.34 -2.62,-0.4 -3.2,-1.53 -0.5,-1 0.13,-1.87 0.6,-2.72 0.17,-0.27 0.37,-0.58 0.63,-0.74 2.85,-1.73 5.35,-4.66 9.25,-2.98 1.3,0.56 2.1,1.86 1.5,3.22 -1.7,3.98 -5.7,4.06 -8.78,4.74z" /> + <!-- Eyes pupils --> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M358.58,262.53c-0.7,3.86 -2.97,7.02 -7.42,8.13 -2.84,0.7 -5.78,1.34 -8.7,1.44 -6.93,0.25 -12.47,3.4 -17.58,7.65 -1.92,1.6 -4.04,2.6 -6.5,3.2 -3.66,0.86 -7.4,0.55 -9.42,-2.37 -2.1,-3.04 -2.2,-6.94 0.05,-10.5 6.9,-10.82 16.5,-17.12 29.6,-17.47 4.03,-0.1 8.12,-0.48 12.12,0.57 4.52,1.2 7.94,5.02 7.86,9.36zM434.48,255.97c-5.6,-0.44 -7.66,-5.72 -4.13,-10.2 2.05,-2.62 4.64,-4.93 8.03,-5.4 8.35,-1.2 16.7,-2.08 24.73,1.9 2.4,1.2 4.23,2.84 5.52,5.06 2.8,4.77 0.28,9.1 -5.3,9.18 -4.65,0.06 -9.3,0.02 -13.95,0.02 0,-0.16 -9.95,-0.17 -14.9,-0.55z" /> + <!--Body shadow --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_2" + android:fillType="evenOdd" + android:pathData="M69.6,356.35c-5.73,-8.7 -19.5,-8.75 -24.82,0.06 -1.18,1.96 -2.48,1.28 -3.7,1 -2.07,-0.45 -4.18,-1.08 -4.1,-3.96 0.18,-5.66 -2.76,-9.96 -6.65,-13.6 -3.22,-3 -7.08,-4.7 -11.7,-3.47 -5.06,1.32 -4.73,-3.32 -6.2,-5.76 -1.56,-2.57 0.45,-3.44 2.56,-4.42 12.08,-5.62 13.63,-18.92 3.07,-27.3 -0.75,-0.6 51.05,47.1 95.77,40.42 18.2,-2.72 42,28.28 71.76,34.45 43.22,8.95 93.8,17.9 92.2,22.07 -4.4,11.42 6,35.6 13,43.43 -0.9,1 -17.88,-14.02 -18.82,-14.62 -8.72,-5.54 -20,-18.82 -28.38,-24.06 -9.33,-5.86 -28.5,-12.07 -43.4,-13.65 -13.66,-1.44 -22.46,-4.1 -37.7,-10.97 -11.65,-5.25 -28.62,-19.36 -31.92,-20.23 -7.04,-1.87 -10.8,-2.45 -14.28,-2.45 -8.64,0 -34.67,5.77 -42.14,3.82 -1.6,-0.02 -3.2,0.57 -4.55,-0.75z" /> + <!-- Body stripes --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd" + android:pathData="M223.93,253.08c-2.75,-0.1 -4.38,-2.87 -2.6,-5.25 3.12,-4.12 6.54,-8.05 10.2,-11.7 1.9,-1.85 4.6,-1.77 6.53,0.45 1.77,2.03 0.77,3.94 -0.83,5.57 -2.84,2.9 -5.7,5.76 -8.6,8.58 -1.3,1.26 -2.8,2.24 -4.7,2.35zM247.83,223.7c-2.6,-0.88 -3.4,-4 -1,-5.77 4.15,-3.08 8.55,-5.9 13.1,-8.36 2.34,-1.27 4.9,-0.43 6.13,2.24 1.12,2.45 -0.38,4 -2.37,5.12 -3.55,2 -7.1,3.94 -10.7,5.83 -1.6,0.84 -3.3,1.37 -5.17,0.94zM283.5,345.85c0.48,2.7 -1.92,4.85 -4.63,3.6 -4.7,-2.18 -9.27,-4.7 -13.6,-7.52 -2.23,-1.45 -2.7,-4.1 -0.9,-6.42 1.63,-2.13 3.7,-1.55 5.64,-0.33 3.45,2.16 6.86,4.35 10.24,6.6 1.5,1 2.78,2.25 3.27,4.08zM255.92,350.23c0.9,2.58 -1.1,5.1 -3.97,4.3 -5,-1.36 -9.94,-3.1 -14.68,-5.15 -2.44,-1.06 -3.35,-3.6 -1.97,-6.2 1.26,-2.36 3.4,-2.14 5.52,-1.25 3.75,1.56 7.48,3.16 11.2,4.82 1.64,0.74 3.1,1.76 3.9,3.48zM245.3,330.43c0.83,2.6 -1.24,5.07 -4.1,4.2 -4.96,-1.5 -9.84,-3.36 -14.53,-5.54 -2.4,-1.14 -3.25,-3.7 -1.8,-6.25 1.32,-2.34 3.46,-2.06 5.55,-1.12 3.7,1.67 7.4,3.36 11.06,5.12 1.62,0.78 3.06,1.84 3.8,3.58z" /> + <!-- Speech bubble inner shape--> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M462.95,64.68c-1.55,-2.12 -3.22,-3.86 -3.53,-6.85 -0.82,-7.74 -5.27,-12.75 -12.78,-14.1 -3.22,-0.57 -4.96,-2 -6.78,-4.42 -8.67,-11.47 -26.16,-10.7 -31.68,2.43 -3.56,8.46 -8.7,14.96 -14.5,21.56 -5.75,6.5 -5.12,11.94 1.3,17.82 6.27,5.75 9.45,5 16.98,3.64 2.18,-0.4 4.95,1.52 7.5,1.46 4.08,-0.1 10.03,-2.13 12.2,-0.14 2.2,2.03 -1.04,4.23 -2.26,6.07 -2.8,4.23 -4,4.35 -7,8.46 -1,1.42 -3.17,3.45 -0.58,5.6 1.96,1.65 5.65,-2.36 7.05,-3.7 5,-4.82 4.7,-6.7 9.7,-11.43 5.57,-5.27 10.23,-6.67 14.48,-10.84 5.1,1.27 7.7,0.17 9.33,-3.08 2.06,-4.1 3.67,-8.22 0.57,-12.47zM411.98,45.38c2.76,-5.76 7.75,-8.18 13.83,-8.8 3.22,-0.15 5.6,1.4 7.7,3.48 2.2,2.14 3.54,4.25 -0.55,5.84 -0.6,0.24 -1.4,0.5 -1.68,1 -5.16,9.36 -12.34,2.07 -18.55,2.1 -2.46,0 -1.3,-2.44 -0.74,-3.63zM404.98,79.25c-2.64,1.52 -4.68,-0.4 -6.36,-2.16 -1.93,-2.04 -4.4,-4.06 -2.78,-7.42 0.57,-1.17 1.38,-2.25 2.18,-3.3 5,-6.56 5.16,-6.5 8.96,0.62 1.2,2.3 2.9,4.33 4.36,6.48 -1.32,3 -3.96,4.37 -6.36,5.77zM410,58.6c-0.4,-3.42 1.83,-3.96 4.83,-3.96 2.74,1.27 8,0.64 8,4.7 0,3.83 -3.1,9.65 -6.55,9.5 -4.17,-0.17 -5.73,-5.78 -6.27,-10.24zM415.82,80.76c-0.5,-0.96 1.04,-3.45 2.08,-3.75 1.42,-0.42 5.46,1.4 5.2,2.84 -0.35,1.78 -6.45,2.54 -7.28,0.92zM423.98,75.23c-0.86,-0.47 -1.5,-0.68 -1.98,-1.1 -1.54,-1.37 -0.2,-2.6 0.64,-3.3 2.8,-2.35 2.5,-7.33 6.7,-8.2 1.1,-0.23 3.15,2 2.64,3.27 -1.6,3.9 -4.77,7.8 -8,9.33zM437.94,75.2c-0.8,2.1 0.13,5.75 -4.05,5.22 -1.54,-0.2 -4.3,-0.78 -3.7,-2.82 0.9,-2.87 3.08,-5.65 6.27,-6.1 2.14,-0.3 1.38,2.15 1.47,3.7zM437.21,61.98c-2.63,-0.27 -5.33,-3.6 -5.4,-6.62 -0.07,-2.52 2.3,-5.23 5.12,-5.1 4.3,0.2 2.6,3.62 2.83,5.9 0.25,2.5 1.2,6.2 -2.54,5.82zM442.89,82.15c-1.2,-0.68 -1.45,-4.35 -0.6,-5.4 1.18,-1.47 6.07,-1.6 6.4,0.25 0.22,1.16 -4.37,5.97 -5.8,5.15zM452.61,65.75c-0.6,2.42 -1,5.7 -3.88,6.2 -2.05,0.34 -4.25,-1.58 -4.45,-3.94 -0.3,-3.65 3.03,-3.6 5.06,-4.4 1.97,-0.14 3.82,-0.04 3.27,2.16zM447.71,58.81c-4.12,1.94 -3.15,-2.85 -3.13,-4.85 0.02,-1.78 -1.6,-5.34 2.1,-4.7 3.42,0.56 7.03,3.04 7.1,6.83 0.04,3.96 -3.94,1.73 -6.08,2.73zM454.51,75.25c-0.13,-4.13 1.48,-5.82 4,-7 0.8,3.95 1.25,5.82 -4,7z" /> +</vector> diff --git a/app/src/main/res/drawable/errorphant_offline.xml b/app/src/main/res/drawable/errorphant_offline.xml new file mode 100644 index 0000000..2a5381a --- /dev/null +++ b/app/src/main/res/drawable/errorphant_offline.xml @@ -0,0 +1,134 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="193dp" + android:height="193dp" + android:viewportWidth="580" + android:viewportHeight="580"> + <!-- Further wire --> + <path + android:fillColor="@color/elephant_friend_accent_color" + android:pathData="M484.83,519.5c-9.1,0 -16.77,-2.14 -19.06,-9.84 0,0 0.63,-5.44 1.63,-6.9 2.34,-3.43 8.96,-4.75 12.83,-4.52 7.96,0.47 27.3,0.4 42.58,-3.43 11.22,-2.8 21.66,-10.72 25.4,-19.24 2.26,-5.1 2,-10.1 -0.76,-15.22 -14.8,-27.44 -61.55,-33.1 -92.48,-36.83l-1.46,-0.17c-20.26,-2.45 -41.82,-4.27 -64.1,-5.4 -1.48,-0.07 -3.16,-0.24 -5.1,-0.43 -3.64,-0.37 -18.88,-1.33 -21.47,-0.03 0,0 -6.8,-13.94 -6.8,-13.94 7.33,-6.93 20.5,-5.6 30.13,-4.65 1.63,0.16 3.17,0.32 4.18,0.37 22.72,1.15 44.72,3 65.4,5.5l1.46,0.2c34.76,4.2 87.3,10.54 106.7,46.5 5.54,10.26 6.04,21.2 1.43,31.66 -6.1,13.88 -21,25.6 -37.96,29.85 -3.72,0.94 -15.95,3.7 -27.8,5.3 -4.94,0.67 -10.04,1.24 -14.74,1.24z" /> + <!-- Shadow and part of the border--> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M340.08,43.43c6.84,-1.48 13.62,-3.16 20.66,-2.8 10.37,0.56 16.8,5.47 19.23,14.64 1.8,6.87 -1.47,11.84 -6.2,16 -2.7,2.4 -3.43,3.6 0.04,6.07 17.86,12.64 25.78,30.78 28.55,51.86 0.65,4.88 5.2,7.9 7.9,11.8 8.66,12.62 15.85,25.7 16.86,41.4 0.4,6.03 -1.05,11.48 -4.56,16.29 -1.45,1.97 -1.8,3.8 -1.27,6.06 1.55,6.5 -0.46,12.4 -3.57,17.9 -3.05,5.4 -7.76,7.14 -14.54,5.33 -3.03,-0.82 -3.9,0 -4.27,2.72 -0.84,6.16 -1.62,12.34 -2.67,18.47 -0.52,3 0.2,6.4 2.47,7.7 8.98,5.22 24.9,-2.13 35.84,-13.28 3.33,-3.38 9.7,-0.18 11.84,4.17 5.44,11.05 2.95,26.28 -5.4,35.32 -6.07,6.55 -17.98,16.66 -28.66,19.96 -2.6,0.8 1.35,6.88 2.37,9.8 3.28,9.44 5.6,19.13 8.1,28.77 0.87,3.37 0.04,5.58 -2.55,8.15 -8.35,8.3 -19,12.35 -29.48,16.6 -3.2,1.3 -3.22,2.14 -2,4.85 1.2,2.72 3.42,4.77 4.38,7.67 1.25,3.8 2.8,7.57 -0.43,10.96 -3.34,3.5 -7.64,4.6 -12.4,3.9 -3.47,-0.5 -6.3,-2.4 -9.2,-4.54 -1.5,9.4 -3.1,18.35 -4.34,27.37 -0.5,3.76 -1.5,7.32 -2.77,10.82 -0.75,2.1 -0.95,3.55 1.77,4.13 4.28,0.9 11.28,4.93 15,6.37 18.76,7.22 30.46,21.74 15.83,33.4 -13.17,10.5 -55.44,18.73 -80.1,22.87 -28.28,4.73 -94.9,-1.1 -110.73,-2.64 -23.76,-2.28 -47.1,-6.92 -70.17,-12.83 -7.97,-2.04 -15.74,-4.92 -23.56,-7.53 -7.15,-2.38 -12.6,-7.4 -17.87,-12.42 -4.15,-3.94 -3.2,-9.32 1.2,-12.9 7.2,-5.9 15.58,-8.94 24.5,-10.8 2.94,-0.63 4.5,-1.58 4.66,-5.06 0.38,-7.64 2.28,-15.1 3.9,-22.54 0.54,-2.5 0.16,-3.97 -2.02,-5.47 -12.7,-8.7 -24.6,-19.2 -29.45,-33.8 -1.9,-5.7 -0.42,-7.23 -4.76,-0.17 -4.36,7.08 -12.4,10.56 -21.1,15.42 -2,0.73 -1.63,2.34 -1.14,3.92 2.64,8.63 -0.4,15.6 -6.52,21.5 -3.2,3.1 -7.1,4.73 -11.52,2.93 -4.25,-1.72 -6,-5.4 -6,-9.7 0,-3.2 -0.9,-3.85 -4.08,-3.45 -6.15,0.77 -8.33,-3.2 -8.35,-9.54 -0.02,-3.7 1.58,-8.67 3.28,-10.26 2.34,-2.2 2,-2.9 -1.76,-5.8 -3.44,-2.66 1.53,-13.03 6.32,-14.9 5.26,-2.08 10.73,-2.6 15.73,0.56 2.3,1.44 3.8,1.1 5.8,-0.23 11.36,-7.38 17.37,-17.8 18.6,-31.25 1.08,-12.07 1.5,-24.15 2.67,-36.24 2.57,-26.28 9.53,-50 24.42,-71.86 9.14,-13.44 18.45,-26.58 30.06,-38 3.15,-3.1 6.58,-5.63 10.8,-7.15 2.5,-0.9 3.5,-2.1 0.7,-4.5 -5.93,-5.13 -6.55,-9.1 -2.78,-16.57 5.93,-11.74 13.8,-22.14 22.52,-31.94 22.3,-25.12 49.2,-44.27 78.63,-60.06 2.86,-1.54 3.8,-2.97 2.53,-6.17 -3.36,-8.43 -3.67,-16.9 -0.03,-25.45 4.4,-10.3 12.12,-15.77 23.13,-15.68 9.7,0.07 16.74,5.4 21.1,13.83 1.66,3.17 2.4,3.92 4.82,0.8 7.7,-9.94 18.35,-12.23 30.12,-11 10.46,1.1 16.24,12.3 11.35,21.7 -0.7,1.33 -1.85,2.5 -1.44,4.5z" /> + <!-- Speech bubble background --> + <path + android:fillColor="@color/elephant_friend_accent_color" + android:fillType="evenOdd" + android:pathData="M560.64,152.37c9.82,18.94 2.83,49.58 -4.2,60.1 -8.5,12.7 -27.84,25.26 -51.5,22.92 -8.3,-0.83 -15.7,-2.08 -26.86,-9.13 -4.44,-2.8 -7.55,-1.95 -14.03,1.87 -6.03,3.56 -16.02,5.18 -18.24,2.8 -3.04,-3.27 1.13,-10.25 4.92,-17.07 3.78,-6.8 5.32,-7.76 3.5,-13.8 -0.57,-1.9 -9.68,-15.8 0.63,-41.7 8.73,-21.9 23.5,-33.83 48.8,-35.7 26.5,-1.96 48.3,12.93 57,29.7z" /> + <!-- Lightnings' border --> + <path + android:fillColor="#FDA201" + android:fillType="evenOdd" + android:pathData="M405.28,504.42c-2.15,-0.07 -4.6,-2.17 -6.58,-4.97 -7.06,-9.93 -10.6,-18.1 -16.95,-28.33 -2,-3.2 -0.6,-6.08 2.34,-7.93 4.3,-2.72 3.24,-4.92 -0.2,-7.4 -5.8,-4.15 -10.93,-9.07 -16.15,-13.93 -5.18,-4.84 -6.54,-10.5 -4.56,-16.73 2.86,-9 6.4,-17.82 9.8,-26.67 1.15,-3.04 2.94,-4.28 6.62,-2.8 10.5,4.2 21.18,7.97 31.83,11.8 4.2,1.52 5.05,3.54 2.2,7.32 -2.97,3.95 -7.53,8.24 -7.6,12.46 -0.1,4.9 7.12,5.92 10.9,8.96 4.64,3.74 6.55,7.12 4.83,13.24 -4.62,16.42 -8.58,33.02 -12.82,49.55 -0.58,2.26 -0.84,4.82 -3.68,5.42zM471.96,422.6c17.67,1.02 17.95,-0.12 14.5,17.04 -1.83,9.04 -3.97,18.03 -5.53,27.1 -0.8,4.7 -3.9,8.87 -8.54,7.78 -10.8,-2.54 -12.4,-6.2 -19.87,10.08 -1.28,2.8 -3.26,12.98 -10,12.97 -3.64,0 -4.74,-3.66 -5.3,-6.76 -1.42,-7.67 -3.84,-25 -4.05,-25.9 -2.27,-9.63 -3.25,-11.82 8.96,-14.76 3.24,-0.78 3.46,-2.55 3.32,-5.1 -0.27,-4.73 -0.4,-9.46 -0.73,-14.18 -0.3,-4.47 1.86,-6.08 6.1,-6.86 7.96,-1.47 14.5,-1.8 21.13,-1.4zM384.9,509.3c-0.2,1.8 0.3,3.7 -1.74,4.45 -1.47,0.52 -2.74,-0.13 -3.86,-1.16 -0.87,-0.8 -1.8,-1.53 -2.74,-2.26 -3.17,-2.5 -6.08,-6.43 -9.6,-7.08 -4.37,-0.8 -5.5,4.9 -8.05,7.73 -3.36,3.76 -6,1.15 -8.5,-0.84 -5.84,-4.68 -8.92,-11.6 -13.57,-17.3 -4.62,-5.66 -9.26,-11.4 -12.23,-18.28 -1.5,-3.48 -1.55,-6.08 1.2,-8.93 5.18,-5.4 11.7,-8.85 18.06,-12.46 3.04,-1.74 6.48,-0.93 8.03,3.2 0.82,2.2 2.07,4.25 3.13,6.36 1.7,3.36 2.58,8.2 5.3,9.66 2.7,1.46 5.6,-3.3 8.74,-4.76 4.2,-1.97 6.78,-0.52 8.22,3.65 4.24,12.4 5.43,25.4 7.6,38z" /> + <!-- Inner orange wires --> + <path + android:fillColor="#FAA102" + android:fillType="evenOdd" + android:pathData="M474.48,503.35c0.82,0.07 2.3,0.08 3.6,3.5 1,2.62 -0.97,6.08 -1.9,6.16 -6.05,0.57 -7.95,2.22 -11.6,6.75 -1.3,1.63 -7.25,6.53 -9.72,5.34 -2.37,-1.17 1.34,-5.9 3.2,-8.44 1.28,-1.74 0.28,-2.6 -1.38,-2.9 -2.37,-0.4 -9.68,2.07 -10.5,-1.48 -0.83,-3.75 7.5,-4.6 9.22,-6.23 -0.7,-2.72 -7.9,-5.96 -5.43,-9.55 1.67,-2.45 7.08,0.52 9.1,2.3 8.12,7.18 9.98,4.07 15.4,4.55zM365.5,547.35c-0.82,0.17 -2.24,0.6 -4.5,-2.27 -1.72,-2.2 -0.87,-6.1 0,-6.46 5.6,-2.33 6.93,-4.47 9.06,-9.88 0.76,-1.94 4.98,-8.4 7.7,-7.98 2.6,0.4 0.47,6 -0.56,9 -0.7,2.04 0.52,2.55 2.18,2.34 2.4,-0.3 8.64,-4.85 10.46,-1.7 1.93,3.34 -5.78,6.63 -6.93,8.7 1.5,2.38 9.3,3.34 8.03,7.5 -0.87,2.84 -6.9,1.6 -9.37,0.5 -9.9,-4.44 -10.74,-0.9 -16.07,0.25z" /> + <!-- Lighter body part --> + <path + android:fillColor="@color/elephant_friend_body_color" + android:fillType="evenOdd" + android:pathData="M309.5,413.94c-3.87,0.1 -6.45,2.94 -9.34,4.83 -17.03,11.15 -35.34,18.28 -55.9,18.72 -5.75,0.12 -11.5,-0.2 -17.05,-2.1 -2.56,-0.88 -3.83,-2.15 -3.78,-5 0.13,-6.13 -1.2,-12.08 -2.72,-17.97 -0.45,-1.77 -1.34,-3.48 -2.3,-5.06 -0.6,-1.02 -1.66,-2.07 -3.1,-1.24 -1.33,0.77 -1.75,1.8 -1.3,3.52 2.83,10.93 1.48,21.9 0.12,32.87 -0.2,1.65 -1.24,2.38 -2.65,2.9 -7.22,2.65 -13.3,6.32 -13.45,15.24 -0.02,0.84 -0.34,1.5 -1.3,1.66 -1.1,0.16 -1.74,-0.42 -2.22,-1.3 -1.18,-2.2 -2.24,-4.44 -3.94,-6.33 -3.93,-4.36 -6.72,-4.9 -11.86,-2.13 -2.92,1.6 -5.4,3.62 -6.9,6.73 -0.58,1.24 -0.52,3.34 -2.9,2.95 -2.17,-0.36 -4.15,-0.6 -5.06,-3.27 -0.84,-2.44 -1.6,-4.95 -3.57,-7 -6.6,-6.84 -12.28,-7.2 -19.33,-0.9 -3.77,3.35 -4.7,3.26 -7.7,-0.93 -1.47,-2.07 -2.2,-4.4 -2.3,-6.98 -0.5,-13.3 0.3,-26.45 5.03,-39.1 0.8,-2.14 -3.3,-6.3 -8.4,-10.03 -9.7,-7.05 -19.72,-18.28 -25.2,-30.94 -1.97,-4.54 -3.15,-13.82 -5.52,-13.86 -1.5,-0.03 -3,6.1 -4.4,8.74 -5.24,9.93 -15.64,13.7 -24.34,19.95 -2.4,1.73 -4.68,0.87 -6.15,-1.87 -2.7,-5.04 -1.26,-9 3.44,-12.1 13.66,-8.98 21.7,-21.55 22.36,-38.14 0.8,-20.28 2.94,-40.34 8.24,-60 6.3,-23.35 16.82,-44.46 32.98,-62.64 4.75,-5.34 8.53,-11.54 14.2,-16.1 1.77,-1.43 3.6,-2.8 6.25,-4.88 -0.8,5.58 -1.58,10.1 -2.08,14.62 -0.84,7.7 3.3,13.64 10.76,14.8 3.47,0.55 3.67,1.72 3.45,4.85 -0.62,8.83 0.8,17.48 8.52,22.98 7.26,5.17 15.44,6.67 23.63,1.6 2.48,-1.52 2.93,0.27 3.74,1.73 6.48,11.82 15.54,21.3 26.44,29.1 0.63,0.45 1.17,1.1 1.86,1.4 2.4,1.05 4.56,-0.35 5.75,-1.8 1.13,-1.4 -0.93,-2.7 -2.18,-3.52 -11,-7.3 -18.04,-17.88 -24.32,-29.1 -2.2,-3.97 -3.03,-7.74 -0.56,-12.28 6.58,-12.12 9.08,-25.38 9.57,-39.06 0.04,-0.93 0.1,-1.88 -0.04,-2.8 -0.18,-1.3 -0.8,-2.5 -2.22,-2.63 -1.25,-0.13 -1.94,0.87 -2.28,1.97 -0.5,1.63 -1.17,3.26 -1.33,4.94 -1.3,14.1 -6.95,26.77 -13.2,39.16 -4.6,9.12 -13.63,11.57 -20.37,6.05 -3.15,-2.58 -4,-6.26 -4.68,-9.9 -0.7,-3.82 -1.34,-7.73 -1.24,-11.58 0.13,-4.95 -2.83,-6.93 -6.73,-7.34 -5.1,-0.53 -6.52,-3.4 -6.6,-7.84 -0.13,-7.76 2.36,-15.05 4.18,-22.43 1.62,-6.58 1.52,-7.28 -4.45,-10.3 -3.03,-1.53 -1.86,-4.2 -1.12,-6.13 1.78,-4.64 3.63,-9.36 6.26,-13.55 7.02,-11.2 14.6,-21.97 24.63,-30.9 16.75,-14.9 33.9,-29.3 53.54,-40.22 6.14,-3.4 12.3,-6.86 18.56,-10.1 2.4,-1.23 1.5,-3.07 1.22,-4.85 -1.08,-7.2 -3.98,-14.1 -2.87,-21.62 2.04,-13.8 10.8,-18.3 22.52,-16 7.66,1.53 11.43,9.06 14.18,16.2 0.8,2.1 0.36,5.04 3.28,5.63 3.02,0.6 4.84,-1.44 6.45,-3.72 3.27,-4.64 7.42,-8.46 12.2,-11.44 6.52,-4.06 13.5,-3.1 20.26,-0.7 3.2,1.13 4,6.4 1.52,9.78 -2.07,2.82 -3.33,5.92 -4.7,9.04 -1.56,3.62 -0.66,5.12 3.37,4.33 7.17,-1.4 14.35,-2.44 21.63,-3.2 5.6,-0.6 10.22,1.23 14.48,4.06 4.25,2.85 2.3,13.23 -2.84,16.8 -5.27,3.67 -5.47,5.68 -0.85,9.95 9.53,8.8 18.57,17.83 23.7,30.2 2.97,7.22 3.93,14.72 5.15,22.2 1.8,11.08 2.67,22.32 1.86,33.52 -0.4,5.8 -0.65,11.78 -2.6,17.4 -1.67,4.87 -0.9,9.9 -0.96,14.84 -0.22,17.26 -1.04,34.42 -3.84,51.54 -2.85,17.4 -6.84,34.4 -11.87,51.3 -5.1,17 -6.27,34.65 -4.46,52.25 0.83,8.02 2.4,16.42 9.23,22.3 0.8,0.7 1.22,1.8 1.76,2.74 1.26,2.2 3.94,4.33 1.67,7.03 -2.07,2.46 -5.18,1.78 -7.9,1.07 -1.45,-0.37 -2.75,-1.38 -4.06,-2.2 -4.9,-3.05 -10.4,-2.58 -14.56,1.34 -1.25,1.17 -2.4,2.46 -3.66,3.6 -1.43,1.3 -2.72,3.8 -4.93,2.56 -1.94,-1.1 -0.64,-3.44 -0.32,-5.2 0.43,-2.27 -0.6,-3.9 -1.94,-5.5 -2.5,-2.96 -4.73,-6.28 -7.65,-8.78 -7,-5.97 -11.13,-13.77 -14.66,-21.9 -3.86,-8.86 -10.1,-16.45 -13.13,-26.13 5.28,-0.78 10.92,-1.33 16.44,-2.48 13.84,-2.9 24.47,-9.7 28.55,-24.22 1.2,-4.32 1.2,-8.65 0.53,-12.96 -0.8,-5.2 -6.34,-8.24 -11.46,-7.1 -13.87,3.05 -27.7,5.82 -41.58,-0.6 -8.8,-4.05 -12.52,-12.2 -17.24,-19.57 -1.2,-1.86 1.15,-3.64 2.17,-5.33 5.57,-9.25 -0.1,-21.56 -10.14,-21.93 -1.08,-0.04 -2.16,0.18 -3.23,0.35 -2.1,0.32 -3.64,1.54 -3.57,3.65 0.08,2.65 2.33,1.85 3.93,1.88 6.58,0.1 8.67,2.55 7.87,9.66 -7.32,-7.13 -14.85,-4.88 -22.26,-0.57 -10.73,6.25 -18.93,14.95 -17.3,28.02 1.53,12.18 7.25,23.1 16.8,31.52 8.72,7.7 18.46,13.6 29.6,17.1 3.44,1.1 7.03,1.16 10.58,1.55 2.46,0.27 3.95,1.04 4.84,3.9 1.32,4.27 3.43,8.4 5.82,12.2 4.5,7.13 8.5,14.5 11.38,22.4 2.88,7.9 8.38,13.7 14.3,19.27 1.56,2.64 0.93,5.4 -0.7,7.44 -7.38,9.28 -14.28,19.06 -24.28,25.93 -1.6,1.1 -3.22,1.76 -5.17,1.47z" /> + <!-- Left for us (closer) tusk --> + <path + android:fillColor="@color/elephant_friend_light_color_2" + android:fillType="evenOdd" + android:pathData="M317.07,314.53c-13.66,0.75 -26.3,-3.13 -37.78,-9.85 -12.04,-7.04 -22.98,-15.73 -26.86,-30.16 -1.56,-5.8 0.3,-11.34 3.68,-16.23 4.2,-6.1 9.77,-10.4 17.12,-12.05 5.1,-1.14 8.46,1.55 9.53,6.67 3.06,14.56 11.45,24.78 25.65,29.78 12.18,4.3 24.4,5.27 36.83,0.5 5.53,-2.13 8.3,0.5 7.78,6.4 -0.9,10.57 -9.3,20.07 -20.77,23.2 -4.96,1.34 -10.05,1.95 -15.2,1.73z" /> + <!-- Right for us (further) tusk --> + <path + android:fillColor="@color/elephant_friend_light_color_1" + android:fillType="evenOdd" + android:pathData="M395.5,296.2c-2.5,-0.17 -3.73,-0.23 -4.97,-0.34 -3.57,-0.3 -4.74,-1.62 -3.26,-5.43 3.45,-8.84 4.5,-13.57 6.96,-22.73 0.78,-2.9 0.77,-4.5 4.75,-3.28 17.9,7.13 27.1,-6.64 38.43,-14.5 3,-1.66 3.58,0.47 4.12,2.45 2.94,10.6 0.68,19.75 -7.23,27.72 -9.67,9.72 -26.16,16.58 -38.8,16.1z" /> + <!-- Right for us (further) leg --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_2" + android:fillType="evenOdd" + android:pathData="M309.5,413.94c10.14,-6.57 17.2,-16.27 25.26,-24.93 2.6,-2.77 3.7,-6.38 4.9,-9.9 3.25,2.58 4.8,5.42 4,10.05 -1.14,6.48 3.7,9.8 9.96,7.72 3.58,-1.2 6.45,-3.52 9.5,-5.76 1.27,1.87 0.52,3.5 0.2,5.1 -2.36,11.73 -6.37,23.07 -8.32,34.9 -0.55,3.4 -2,5.23 -6.44,4.14 -7.45,-1.82 -14.2,2.76 -16.94,10.47 -0.67,1.88 -0.63,4.34 -3.46,4.18 -2.48,-0.14 -1.88,-2.54 -2.33,-4 -2.62,-8.46 -9.3,-10.9 -16.52,-5.8 -2.64,1.88 -5.3,3.9 -6.54,7.1 -0.68,1.76 -1.94,2.46 -3.63,2.2 -5.33,-0.8 -10.78,-0.52 -16,-2.66 -3.63,-1.5 -3.43,-4.38 -4,-7.2 -0.64,-3.1 1.9,-3.5 3.8,-4.4 8.86,-4.17 17.67,-8.44 25.74,-14.07 3.87,-2.7 4.02,-4.05 0.84,-7.1z" /> + <!-- Right for us (further) hand and ear--> + <path + android:fillColor="@color/elephant_friend_dark_body_color_2" + android:fillType="evenOdd" + android:pathData="M379.6,334.7c-0.2,-9.38 1.5,-18.86 3.34,-28.32 0.45,-2.32 1.66,-3 4.03,-2.27 4.53,1.42 9.2,1.6 13.83,0.6 2.26,-0.5 3.46,0.2 4.2,2.4 2.85,8.54 6.5,16.82 8.36,25.7 0.46,2.2 0.65,3.67 -2.1,4.6 -6.25,2.13 -9.97,7.83 -9.58,14.27 0.1,1.48 0.45,2.7 -1.23,3.5 -1.62,0.8 -2.92,0.6 -4.2,-0.65 -3.75,-3.67 -8.17,-4.95 -13.25,-3.43 -2.06,0.6 -2.27,-0.7 -2.65,-2.15 -1.2,-4.6 -0.3,-9.3 -0.75,-14.26zM404.8,146c4.76,5.9 7.96,12.4 10.48,19.1 2.74,7.28 4.54,14.88 3.68,22.82 -0.3,2.85 -0.9,5.56 -4,6.82 -3.28,1.34 -3.4,3.47 -1.8,6.34 3.14,5.62 2.77,11.32 -0.07,16.96 -1.5,2.96 -8.94,4.9 -11.66,3.04 -1.6,-1.1 -1.1,-2.83 -0.97,-4.3 1.23,-14.5 2.45,-29.04 3.86,-43.55 0.7,-7.15 -0.15,-14.25 -0.17,-21.37 0,-1.78 -0.6,-3.66 0.63,-5.87z" /> + <!-- Lil' thing on a tail :3 --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_2" + android:fillType="evenOdd" + android:pathData="M51.52,396.74c0.43,-2.54 1.1,-6.2 1.68,-9.9 0.2,-1.23 0.3,-2.63 -1.12,-3.23 -1.57,-0.64 -2.23,0.58 -2.75,1.76 -1,2.3 -2.17,4.36 -4.08,5.67 -1.14,0.78 -3.46,0.75 -4.78,-0.32 -1.45,-1.15 -1.46,-4.5 -0.8,-5.66 1.9,-3.2 4.4,-4.7 7.1,-7.43 2.95,-2.97 2.54,-4.02 -1.5,-5.1 -2.06,-0.56 -4.63,-0.7 -4.78,-3.57 -0.15,-2.66 1.78,-4.14 3.87,-5.36 1.8,-1.05 3.76,-1.4 5.83,-1.27 5.07,0.3 5.65,1.1 4.22,6 -2.34,8 -1.04,11.46 6.18,16.34 2.75,1.86 4.46,8.95 2.84,12.9 -1.3,3.23 -3.68,5.8 -6.4,7.9 -2.08,1.63 -3.76,0.78 -4.77,-1.54 -0.84,-1.93 -0.64,-3.97 -0.75,-7.16z" /> + <!-- Lil' fingers --> + <path + android:fillColor="@color/elephant_friend_light_color_1" + android:fillType="evenOdd" + android:pathData="M312.62,450.27c-2.06,-0.46 -5.12,0.7 -5.7,-1.1 -0.82,-2.5 2.33,-3.64 4.03,-5 3.94,-3.15 6.66,-2.98 8.7,0.15 2.13,3.22 1.33,5.05 -2.53,5.63 -1.53,0.23 -3.1,0.22 -4.5,0.32zM157.95,460.62c-5.3,-0.4 -9.72,-1.5 -14.06,-2.98 -2.02,-0.67 -1.86,-1.5 -0.45,-2.87 4.25,-4.1 8.5,-3.86 11.94,0.94 0.94,1.33 1.52,2.9 2.55,4.92zM174.74,463.44c1.64,-3.02 2.93,-4.8 5.17,-5.88 4.1,-1.94 7.56,-0.25 8.52,4.18 0.42,1.9 -0.87,1.67 -1.87,1.68 -3.67,0.06 -7.34,0.02 -11.8,0.02zM336.16,448.93c1.17,-5.2 4.6,-7.28 9.25,-7.9 0.8,-0.1 1.53,0.08 2.07,0.75 0.48,0.62 0.23,1.2 -0.2,1.63 -3.03,2.96 -5.6,6.8 -11.1,5.53zM391.76,359.1c-3.24,1.28 -6.07,1.93 -8.98,0.53 -0.94,-0.45 -2.2,-0.8 -1.97,-2.2 0.18,-1.06 1.22,-1.28 2.12,-1.5 3.43,-0.87 6.42,-0.48 8.84,3.18zM406.75,352.33c0,-5.14 1.85,-7.8 5.74,-8.94 0.8,-0.24 1.93,-0.62 2.46,0.5 0.4,0.8 -0.26,1.53 -0.8,2.02 -2.18,1.95 -4.42,3.83 -7.4,6.4zM209.56,454.67c-0.23,2.68 -3.84,6.76 -5.77,6.53 -1,-0.12 -1.2,-0.7 -1.16,-1.6 0.1,-1.9 4.42,-6.72 5.9,-6.55 1.04,0.12 1.15,0.78 1.02,1.62z" /> + <!-- Cross inside the speech bubble --> + <path + android:fillColor="#F45353" + android:fillType="evenOdd" + android:pathData="M534.5,201.3c-0.05,1.62 -5.06,4.07 -6.87,5.16 -3.47,2.08 -6.9,4.23 -10.5,6.06 -3.32,1.7 -6.5,1.46 -8.1,-2.47 -1.65,-4.02 -6.47,-13.87 -6.47,-13.87s-8.8,5.63 -10.9,7.5c-2.45,2.23 -5.3,2.47 -7.54,0.35 -3.12,-2.97 -5.98,-6.35 -7.63,-10.44 -2.1,-5.12 -0.73,-8.4 4.3,-10.58 9.5,-4.25 8.57,-4.25 8.57,-4.25s0.94,0 -5.7,-8.16c-1.05,-1.52 -2.4,-2.9 -3.23,-4.53 -1.7,-3.44 -1.48,-6.54 2.1,-8.92 4.24,-2.83 8.37,-5.84 12.6,-8.72 3.92,-2.68 7.6,-1.25 8.83,3.33 0.27,1.03 4.82,13.3 4.82,13.3s3.28,-3.1 6.2,-6.6c2.96,-3.53 5.6,-6.68 9,-7.9 2.28,-0.84 5.9,1.65 7.12,3.17 3.27,4.1 7.4,8.32 10.23,12.75 2.3,3.6 0.2,6.83 -4,9.32 -3.86,2.27 -10.4,5.63 -14.6,7.62 3.72,4.45 8.52,10.54 9.96,12.94 0.98,1.6 1.78,2.35 1.8,4.95z" /> + <!-- Inner lightning part --> + <path + android:fillColor="#FDED01" + android:fillType="evenOdd" + android:pathData="M392.36,474.27c-1.67,-3.3 -3.37,-5.32 2.46,-9.24 4.87,-3.28 3.88,-4.96 0.05,-8.64 -6.18,-5.92 -13.7,-11.35 -19.86,-16.35 -4.12,-3.36 -5.04,-8.04 -3.25,-12.74 1.84,-4.86 3.3,-9.92 4.43,-15 0.88,-3.92 1.57,-9.7 13.2,-4.5 16.55,7.4 13.85,9.62 8.22,20.8 -3.05,6.05 -2.17,9.5 4.6,11.84 9.85,4.25 12.58,6.6 11.1,14.06 -2.53,12.68 -7.05,41.66 -9.94,41.67 -2.45,0 -7.93,-15.82 -11,-21.9zM443.2,491.03c-2.34,-0.08 -2.85,-19.2 -3.6,-25.64 -0.4,-3.54 3.07,-6.3 5.6,-6.9 10.02,-2.28 10.67,-0.48 8.92,-10.75 -0.12,-0.7 -0.34,-1.38 -0.45,-2.08 -1.47,-9.52 -1,-11.12 8.5,-12.67 16.6,-2.7 17.4,-0.98 14.57,15.65 -0.7,4.2 -1.68,8.4 -2.1,12.63 -0.45,4.57 -2.8,6.07 -7.07,5.62 -12.44,-1.3 -14.94,2.13 -18.24,11.3 -0.83,2.32 -4.22,12.9 -6.12,12.83zM380.22,506.38c-2.67,1.2 -10.35,-9.17 -13.22,-10.96 -1.65,-1.02 -5.03,-2.06 -10.26,7.06 -1.2,2.94 -2.98,1.02 -4.18,-0.6 -6.08,-8.14 -12.07,-16.36 -18.26,-24.42 -2.83,-3.67 -5.9,-6.33 2.7,-11.64 9.4,-5.8 9.5,-5.05 16.23,9.2 3.83,8.1 6.77,9.04 12.77,3.96 2.78,-2.24 4.18,-1.42 5.24,1.4 2.14,5.7 11.4,24.9 8.98,26z" /> + <!-- Closer arm border and chest thingy --> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M210.54,346.53c-1.04,-10.87 -1.55,-21.76 -1,-32.67 0.3,-6.02 0.06,-12.27 3.43,-17.66 0.7,-1.14 0.3,-4.48 3.4,-2.9 2.4,1.2 3.74,3.16 2.36,5.73 -3.3,6.14 -2.77,12.68 -2.3,19.18 0.75,10.23 1.36,20.45 1.57,30.7 0.13,6.7 -1.3,13.33 -2.94,19.6 -1.1,4.25 -6.76,6.1 -11.14,7.7 -14.25,5.15 -28.7,4.03 -43.18,1.05 -4.4,-0.9 -8.8,-1.9 -13.13,-3.14 -12.8,-3.64 -21.36,-14.9 -22.23,-29.2 -1.33,-21.93 3.8,-42.26 15.37,-60.96 2.07,-3.34 4.55,-6.32 7.7,-8.7 1.12,-0.88 2.48,-1.9 3.7,-0.52 1.15,1.3 0.18,2.68 -0.76,3.74 -11.3,12.77 -15.1,28.64 -18.66,44.64 -2.04,9.1 -0.17,17.94 1.43,26.8 0.28,1.57 1,2.22 2.96,1.74 10.44,-2.54 18.43,3.77 19.02,14.98 0.1,2.15 -0.02,3.95 2.7,4.6 2.68,0.6 3.1,-1.24 3.93,-2.92 4.6,-9.3 13.3,-10.46 20.12,-2.64 1.32,1.5 2.54,3.24 3.26,5.08 1.04,2.65 2.88,2.18 4.92,2 2.46,-0.2 1.8,-1.85 1.96,-3.36 0.86,-7.92 7.08,-13.4 14.82,-12.84 2.32,0.17 2.73,-0.7 2.72,-2.53 0,-2.5 0,-4.98 0,-7.47zM308.06,332.2c-0.12,6.25 -2.05,10.53 -6.22,13.6 -1.52,1.1 -3.7,2.1 -5.26,1.3 -4.14,-2.1 -6.18,-0.1 -8.64,2.88 -3.26,3.96 -7.98,4.98 -12.76,3.6 -4.4,-1.26 -5.85,-5.44 -6.63,-9.4 -0.6,-3 -1.15,-3.68 -4.4,-2.7 -9.76,2.93 -14.16,-1.63 -12,-11.66 1,-4.58 2.56,-8.65 6.57,-11.36 1.13,-0.77 2.5,-1.76 3.74,-0.3 1,1.17 0.15,2.4 -0.62,3.36 -2.62,3.28 -3.73,7.05 -4.08,11.2 -0.23,2.62 0.6,3.44 3.14,2.85 2,-0.47 3.8,-1.23 5.64,-2.13 5.07,-2.5 7.27,-1.12 7.55,4.4 0.08,1.86 0.33,3.73 0.8,5.5 1.24,4.95 4.83,5.96 8.48,2.23 1.82,-1.86 3.06,-4.3 4.58,-6.44 1.9,-2.7 4,-3.1 6.62,-0.78 3.23,2.9 4.34,2.52 6.06,-1.47 0.78,-1.8 0.67,-3.66 0.7,-5.5 0,-2.2 0.3,-4.33 2.96,-4.42 2.47,-0.08 3.38,1.84 3.66,4.04 0.1,0.77 0.13,1.54 0.1,1.2z" /> + <!-- Trunk stripe --> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M372.7,246.14c-6.72,-2.18 -13.42,0 -20.2,1.85 -2.07,0.55 -4.3,0.64 -6.45,0.8 -0.93,0.06 -1.9,-0.37 -2.2,-1.4 -0.32,-1.08 0.37,-1.85 1.22,-2.25 8.34,-3.92 16.8,-7.33 26.32,-6.44 1.6,0.15 2.97,0.8 4.15,1.9 1.13,1.08 1.6,2.3 1,3.8 -0.63,1.53 -1.94,1.88 -3.84,1.74z" /> + <!-- Closer eye --> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M286.42,191.5c-0.25,5.12 -3.8,9 -7.9,8.64 -4.02,-0.35 -7.08,-3.9 -6.9,-8.03 0.2,-4.75 4.3,-8.5 8.97,-8.18 3.56,0.24 6.02,3.44 5.82,7.6z" /> + <!-- Further eye --> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M386.43,187.58c-0.06,3.9 -3.55,7.14 -7.63,7.06 -3.82,-0.08 -6.46,-3 -6.34,-7.05 0.14,-4.75 3.54,-8.84 7.25,-8.73 3.3,0.1 6.8,4.63 6.73,8.7z" /> + <!-- Body stripe --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd" + android:pathData="M220.7,121.5c-3.06,4.42 -5.13,10.57 -10.46,14.47 -1.03,0.75 -2.34,1 -3.45,0 -1.06,-0.92 -1.27,-2.26 -0.8,-3.42 2.25,-5.68 5.84,-10.54 9.74,-15.17 0.84,-1 2.2,-1.16 3.44,-0.54 1.5,0.76 1.54,2.2 1.52,4.66z" /> + <!-- Trunk stripe --> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M356.87,266.8c-0.66,-0.13 -1.88,-0.28 -3.06,-0.6 -0.9,-0.26 -1.68,-0.86 -1.7,-1.94 0,-1.05 0.65,-1.7 1.6,-2.05 4.9,-1.86 9.88,-3.6 15.1,-4.36 2.24,-0.33 3.98,0.7 4.4,3.1 0.47,2.52 -1.06,3.74 -3.33,4.08 -4.13,0.62 -8.28,1.13 -13,1.77z" /> + <!-- Body stripes --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd" + android:pathData="M189.06,292.78c1.26,-4.3 3.43,-8.9 6.02,-13.28 0.6,-1 1.7,-1.37 2.85,-0.8 1.37,0.66 2.28,1.83 1.88,3.35 -1.2,4.63 -2.33,9.3 -4.73,13.5 -0.8,1.4 -2.04,2.48 -3.84,1.68 -1.57,-0.7 -2.17,-2.1 -2.17,-4.45zM198.63,185.98c-0.63,4.92 -2.85,9.2 -4.9,13.5 -0.66,1.33 -2.2,1.65 -3.68,1 -1.26,-0.58 -1.86,-1.58 -1.68,-3 0.62,-4.82 2.53,-9.23 4.37,-13.64 0.58,-1.4 1.85,-2.1 3.42,-1.76 2.12,0.5 2.25,2.3 2.47,3.9zM182.57,137.72c-2.2,-0.14 -3.55,-1.7 -2.52,-4 2.05,-4.5 4.26,-9 8.27,-12.18 1.23,-0.97 2.72,-1.88 4.22,-0.62 1.46,1.23 1.03,2.8 0.14,4.2 -2.14,3.43 -4.27,6.86 -6.46,10.25 -0.75,1.17 -1.6,2.33 -3.65,2.35zM129.1,250.15c1.57,-3.46 2.86,-7.78 5.6,-11.37 0.87,-1.13 2.3,-1.77 3.74,-1 1.53,0.85 2.22,2.4 1.6,3.97 -1.57,4.02 -3.28,8 -5.15,11.87 -0.62,1.28 -2.08,2 -3.6,1.42 -1.85,-0.7 -2.23,-2.38 -2.2,-4.9zM104.77,259.68c1.02,-4.7 3.63,-8.9 6.3,-13.1 0.7,-1.1 2.08,-1.3 3.35,-0.68 1.3,0.64 1.7,1.9 1.44,3.15 -1,5.1 -3.78,9.45 -6.2,13.92 -0.62,1.14 -2.1,1.3 -3.34,0.63 -1.46,-0.77 -1.5,-2.22 -1.55,-3.92zM196.4,405.28c0.33,-2.06 -0.65,-5.15 2.86,-5.25 3,-0.1 3.44,2.43 3.8,4.9 0.45,3.2 1.3,6.35 1.77,9.56 0.22,1.56 -0.16,3.2 -2.06,3.7 -1.83,0.46 -2.9,-0.64 -3.7,-2.1 -1.82,-3.35 -2.6,-6.95 -2.66,-10.82zM179.67,166.87c0.44,-5.7 2.9,-9.64 5.6,-13.45 0.94,-1.34 2.5,-1.94 4.1,-1.02 1.45,0.83 1.62,2.32 1.07,3.68 -1.54,3.88 -3.14,7.75 -4.92,11.52 -0.7,1.5 -2.18,2.23 -3.92,1.72 -1.65,-0.48 -1.94,-1.82 -1.93,-2.45zM189.68,317.63c-0.5,3.56 -0.88,7.1 -1.57,10.57 -0.33,1.66 -1.24,3.36 -3.44,3.03 -1.94,-0.3 -2.9,-1.72 -3.07,-3.58 -0.45,-4.6 1.1,-8.85 2.43,-13.12 0.48,-1.52 1.94,-1.97 3.45,-1.5 2.4,0.72 1.8,2.93 2.2,4.6zM130.33,224.68c-2.6,-0.24 -3.46,-1.8 -2.67,-3.9 1.44,-3.83 3.92,-7.07 6.97,-9.77 1.3,-1.13 3.18,-1.62 4.7,-0.2 1.46,1.4 1.22,3.15 0.2,4.7 -1.55,2.33 -3.16,4.63 -4.87,6.84 -1.1,1.4 -2.68,2.08 -4.33,2.34zM169.37,397.9c0,-1.08 -0.06,-2.17 0.02,-3.25 0.07,-1.35 0.73,-2.4 2.1,-2.73 1.48,-0.36 2.36,0.6 2.94,1.74 1.9,3.8 2.58,7.94 2.93,12.12 0.12,1.52 -0.92,2.72 -2.54,2.78 -1.8,0.08 -3.44,-0.6 -4.08,-2.46 -0.92,-2.64 -1.56,-5.36 -1.38,-8.2zM117.86,285.05c-0.43,3.52 -2.02,6.88 -3.73,10.17 -0.63,1.2 -1.73,2.28 -3.42,1.68 -1.5,-0.54 -2.22,-1.76 -2.12,-3.26 0.3,-4.27 2.1,-8.04 4.38,-11.56 0.74,-1.13 2.13,-1.3 3.38,-0.83 1.64,0.63 1.5,2.15 1.52,3.8zM175.57,295.8c-1.7,4.22 -3.23,8.28 -5,12.25 -0.47,1.1 -1.8,1.05 -2.93,0.7 -1.17,-0.34 -2.04,-1.23 -1.84,-2.38 0.78,-4.43 1.38,-8.97 4.38,-12.62 0.95,-1.15 2.26,-1.74 3.74,-0.86 1.15,0.68 1.8,1.7 1.65,2.9z" /> + <!-- Body stripe --> + <path + android:fillColor="@color/elephant_friend_dark_body_color_1" + android:fillType="evenOdd" + android:pathData="M181.5,425.72c0,-0.68 -0.03,-1.15 0.02,-1.6 0.25,-2.13 0.2,-4.94 3,-4.93 2.4,0 3.3,2.43 3.5,4.74 0.2,2.6 1.3,5.1 0.78,7.77 -0.3,1.64 -1.06,3.03 -2.83,3.24 -1.84,0.22 -2.82,-1.08 -3.32,-2.63 -0.7,-2.2 -1.3,-4.44 -1.14,-6.58z" /> + <!-- Trunk stripe --> + <path + android:fillColor="@color/elephant_friend_border_color" + android:fillType="evenOdd" + android:pathData="M366.4,283.86c-1.87,0.13 -3.4,-0.38 -4.03,-2.33 -0.58,-1.8 0.6,-2.86 1.86,-3.84 2.32,-1.82 5.02,-2.47 7.84,-2.08 1.95,0.26 3.48,1.53 3.62,3.64 0.14,2.24 -1.35,3.3 -3.4,3.63 -1.97,0.33 -3.94,0.64 -5.9,0.96z" /> + <!-- Closer arm fingers --> + <path + android:fillColor="@color/elephant_friend_light_color_1" + android:fillType="evenOdd" + android:pathData="M140.6,354.95c4.73,0.06 9.56,5.57 9.4,10.45 -0.05,1.58 -0.64,2.17 -1.87,1.56 -4.02,-2.02 -8.16,-3.98 -10.1,-8.5 -1.2,-2.77 -0.6,-3.54 2.58,-3.5zM166.22,371.58c0.93,-3.5 3.02,-5.46 6.4,-5.54 4.37,-0.1 5.93,3.26 7.34,6.6 -1.4,0.72 -9.28,0.2 -13.74,-1.06zM198.66,371.1c-1.6,-0.1 -1.43,-0.54 -1.37,-1.52 0.13,-2.68 5,-7.9 7.56,-8.02 1.67,-0.07 3.13,0.75 3.68,2.27 2.08,5.77 -7.72,7.4 -9.88,7.28z" /> + <!-- ??? What's this?--> + <path + android:fillColor="#ADC7BA" + android:fillType="evenOdd" + android:pathData="M175.7,372.76c1.42,-0.03 2.84,-0.07 4.26,-0.1" /> + <!-- Closer cable part --> + <path + android:fillColor="@color/elephant_friend_accent_color" + android:pathData="M288.17,570.5c-12.3,0 -24.53,-1.8 -35.3,-6.22 -2.82,-1.16 -8.25,-3.4 -13.7,-7.3 -9.7,-1.62 -19.28,-4.92 -27.95,-9.94 -17.26,-10 -34.4,-25.67 -48.2,-38.26 -3.87,-3.54 -7.52,-7.78 -10.6,-10.45 -11.05,-9.57 -41.26,-24.33 -63.54,-24.33 -0.05,0 -0.1,0 -0.13,0 -21.1,0 -49.48,10.67 -61.18,26.37 -1.56,2.08 -5.9,5.54 -11.53,3.2 -6.9,-2.9 -7.12,-8.33 -5.3,-12.35C18.97,472.94 62.47,456 88.7,456c0.06,0 0.1,0 0.16,0 25.63,0 60.5,15.28 75.8,28.55 3.27,2.82 7.02,6.52 11,10.15 13.1,11.98 29.42,27.04 44.93,36 0.98,0.6 2,1.2 3,1.7 -2.25,-19.02 8.88,-44.9 38.96,-54.04 12.8,-3.88 25.97,2.37 34.4,16.27 9.1,14.93 14,33.56 0.5,47.4 -3.85,3.93 -8.5,7.16 -13.73,9.66 16.06,0.66 33,-2.57 45.4,-6.66 5.04,-1.65 14.67,-5.88 18.98,-7.65 5.9,-2.44 9.57,-3.24 14.94,-4.05 5.32,-0.8 7.05,12.32 5.2,15.9 -0.56,1.08 -8.24,3.48 -11.37,4.67 -1.68,0.64 -16.2,7.03 -21.88,8.9 -13.27,4.37 -30.1,7.67 -46.83,7.67zM246.81,539.1c14.85,1.46 29.3,-1.97 37.26,-10.14 6.54,-6.7 2.2,-16.86 -3.07,-25.54 -1.46,-2.37 -6.66,-9.04 -13,-7.12 -21.37,6.48 -27.76,24.98 -25.65,34.94 0.6,2.9 2.06,5.48 4.46,7.85z" /> +</vector> diff --git a/app/src/main/res/drawable/help_message_background.xml b/app/src/main/res/drawable/help_message_background.xml new file mode 100644 index 0000000..cb28b79 --- /dev/null +++ b/app/src/main/res/drawable/help_message_background.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:bottom="1dp" + android:left="-1dp" + android:right="-1dp" + android:top="1dp"> + + <shape android:shape="rectangle"> + <solid android:color="?attr/colorBackgroundAccent"/> + <stroke android:width="1dp" android:color="?attr/dividerColor"/> + </shape> + </item> +</layer-list> diff --git a/app/src/main/res/drawable/ic_access_time.xml b/app/src/main/res/drawable/ic_access_time.xml new file mode 100644 index 0000000..2239a4f --- /dev/null +++ b/app/src/main/res/drawable/ic_access_time.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_account_settings.xml b/app/src/main/res/drawable/ic_account_settings.xml new file mode 100644 index 0000000..d13907d --- /dev/null +++ b/app/src/main/res/drawable/ic_account_settings.xml @@ -0,0 +1,13 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:autoMirrored="true" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillAlpha="1" + android:fillColor="#000000" + android:pathData="M8.9987,3.9987C6.7925,3.9987 5,5.7913 5,7.9975C5,10.21 6.7925,12.0025 8.9987,12.0025C11.2113,12.0025 12.9975,10.21 12.9975,7.9975C12.9975,5.7913 11.2113,3.9987 8.9987,3.9987ZM8.9987,13.9987C6.3288,13.9987 1.0013,15.3325 1.0013,17.9975L1.0013,20L12.08,20C12.0262,19.6675 12.0025,19.3362 12.0025,18.9988C12.0025,17.485 12.495,16.0113 13.4087,14.8C11.88,14.2775 10.1813,13.9987 8.9987,13.9987ZM17.9887,13.9987C17.8712,13.9987 17.7588,14.0912 17.7388,14.2087L17.5487,15.5275C17.2512,15.6588 16.9575,15.82 16.6988,16.02L15.4587,15.5175C15.3512,15.4787 15.22,15.5225 15.1512,15.63L14.15,17.3587C14.0913,17.4713 14.1112,17.5975 14.2087,17.6812L15.2688,18.5113C15.2487,18.6712 15.2388,18.8275 15.2388,18.9988C15.2388,19.17 15.2487,19.3312 15.2688,19.4925L14.2087,20.3225C14.1212,20.4 14.0913,20.5325 14.15,20.64L15.1512,22.3687C15.21,22.48 15.3413,22.52 15.4587,22.48L16.6988,21.9825C16.9575,22.1825 17.2412,22.3488 17.5487,22.4713L17.7388,23.7888C17.7588,23.9113 17.8612,23.9988 17.9887,23.9988L19.99,23.9988C20.1123,23.9988 20.22,23.9113 20.2393,23.7888L20.4297,22.4713C20.7275,22.3387 21.0205,22.1825 21.27,21.9825L22.52,22.48C22.6312,22.52 22.7588,22.48 22.8325,22.3687L23.8288,20.64C23.8913,20.5325 23.8575,20.4 23.77,20.3225L22.7002,19.4925C22.72,19.3312 22.7393,19.17 22.7393,18.9988C22.7393,18.8275 22.7295,18.6712 22.7002,18.5113L23.76,17.6812C23.8475,17.5975 23.8812,17.4713 23.8188,17.3587L22.8225,15.63C22.7588,15.5225 22.6312,15.4787 22.51,15.5175L21.27,16.02C21.0107,15.82 20.7275,15.65 20.42,15.5275L20.2295,14.2087C20.22,14.0912 20.1123,13.9987 19.99,13.9987M18.9888,17.5C19.8188,17.5 20.4888,18.1687 20.4888,18.9988C20.4888,19.8288 19.8188,20.4975 18.9888,20.4975C18.1588,20.4975 17.49,19.8288 17.49,18.9988C17.49,18.1687 18.1588,17.5 18.9888,17.5Z" + android:strokeWidth="0.2" + android:strokeLineJoin="round" /> +</vector> diff --git a/app/src/main/res/drawable/ic_add_a_photo_32dp.xml b/app/src/main/res/drawable/ic_add_a_photo_32dp.xml new file mode 100644 index 0000000..172c5ac --- /dev/null +++ b/app/src/main/res/drawable/ic_add_a_photo_32dp.xml @@ -0,0 +1,4 @@ +<vector android:height="32dp" android:viewportHeight="24.0" + android:viewportWidth="24.0" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#FFFFFFFF" android:pathData="M3,4L3,1h2v3h3v2L5,6v3L3,9L3,6L0,6L0,4h3zM6,10L6,7h3L9,4h7l1.83,2L21,6c1.1,0 2,0.9 2,2v12c0,1.1 -0.9,2 -2,2L5,22c-1.1,0 -2,-0.9 -2,-2L3,10h3zM13,19c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5 -5,2.24 -5,5 2.24,5 5,5zM9.8,14c0,1.77 1.43,3.2 3.2,3.2s3.2,-1.43 3.2,-3.2 -1.43,-3.2 -3.2,-3.2 -3.2,1.43 -3.2,3.2z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_alert_circle.xml b/app/src/main/res/drawable/ic_alert_circle.xml new file mode 100644 index 0000000..4c894f0 --- /dev/null +++ b/app/src/main/res/drawable/ic_alert_circle.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="#000" android:pathData="M11,15H13V17H11V15M11,7H13V13H11V7M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_arrow_back_with_background.xml b/app/src/main/res/drawable/ic_arrow_back_with_background.xml new file mode 100644 index 0000000..5253e68 --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_back_with_background.xml @@ -0,0 +1,13 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32dp" + android:height="32dp" + android:autoMirrored="true" + android:viewportWidth="32" + android:viewportHeight="32"> + <path + android:fillColor="@color/toolbar_icon_background" + android:pathData="M16 0C7.152 0 0 7.152 0 16s7.152 16 16 16 16-7.152 16-16S24.848 0 16 0z" /> + <path + android:fillColor="?attr/colorControlNormal" + android:pathData="M24 15H11.83l5.59-5.59L16 8l-8 8 8 8 1.41-1.41L11.83 17H24v-2z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_attach_file_24dp.xml b/app/src/main/res/drawable/ic_attach_file_24dp.xml new file mode 100644 index 0000000..806cac0 --- /dev/null +++ b/app/src/main/res/drawable/ic_attach_file_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#fff" + android:pathData="M16.5,6v11.5c0,2.21 -1.79,4 -4,4s-4,-1.79 -4,-4V5c0,-1.38 1.12,-2.5 2.5,-2.5s2.5,1.12 2.5,2.5v10.5c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1V6H10v9.5c0,1.38 1.12,2.5 2.5,2.5s2.5,-1.12 2.5,-2.5V5c0,-2.21 -1.79,-4 -4,-4S7,2.79 7,5v12.5c0,3.04 2.46,5.5 5.5,5.5s5.5,-2.46 5.5,-5.5V6h-1.5z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_bookmark_24dp.xml b/app/src/main/res/drawable/ic_bookmark_24dp.xml new file mode 100644 index 0000000..803bca9 --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="?android:attr/textColorTertiary" + android:pathData="M17,3L7,3c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3L19,5c0,-1.1 -0.9,-2 -2,-2zM17,18l-5,-2.18L7,18L7,5h10v13z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_bookmark_active_24dp.xml b/app/src/main/res/drawable/ic_bookmark_active_24dp.xml new file mode 100644 index 0000000..217b78b --- /dev/null +++ b/app/src/main/res/drawable/ic_bookmark_active_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#19a341" + android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_bot_24dp.xml b/app/src/main/res/drawable/ic_bot_24dp.xml new file mode 100644 index 0000000..26d4c9e --- /dev/null +++ b/app/src/main/res/drawable/ic_bot_24dp.xml @@ -0,0 +1,8 @@ +<!-- drawable/robot.xml --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="#000" android:pathData="M12,2A2,2 0 0,1 14,4C14,4.74 13.6,5.39 13,5.73V7H14A7,7 0 0,1 21,14H22A1,1 0 0,1 23,15V18A1,1 0 0,1 22,19H21V20A2,2 0 0,1 19,22H5A2,2 0 0,1 3,20V19H2A1,1 0 0,1 1,18V15A1,1 0 0,1 2,14H3A7,7 0 0,1 10,7H11V5.73C10.4,5.39 10,4.74 10,4A2,2 0 0,1 12,2M7.5,13A2.5,2.5 0 0,0 5,15.5A2.5,2.5 0 0,0 7.5,18A2.5,2.5 0 0,0 10,15.5A2.5,2.5 0 0,0 7.5,13M16.5,13A2.5,2.5 0 0,0 14,15.5A2.5,2.5 0 0,0 16.5,18A2.5,2.5 0 0,0 19,15.5A2.5,2.5 0 0,0 16.5,13Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_briefcase.xml b/app/src/main/res/drawable/ic_briefcase.xml new file mode 100644 index 0000000..6df5b88 --- /dev/null +++ b/app/src/main/res/drawable/ic_briefcase.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="18dp" + android:height="18dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <path + android:fillColor="@color/textColorTertiary" + android:pathData="M20,6C20.58,6 21.05,6.2 21.42,6.59C21.8,7 22,7.45 22,8V19C22,19.55 21.8,20 21.42,20.41C21.05,20.8 20.58,21 20,21H4C3.42,21 2.95,20.8 2.58,20.41C2.2,20 2,19.55 2,19V8C2,7.45 2.2,7 2.58,6.59C2.95,6.2 3.42,6 4,6H8V4C8,3.42 8.2,2.95 8.58,2.58C8.95,2.2 9.42,2 10,2H14C14.58,2 15.05,2.2 15.42,2.58C15.8,2.95 16,3.42 16,4V6H20M4,8V19H20V8H4M14,6V4H10V6H14Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_bullhorn_24dp.xml b/app/src/main/res/drawable/ic_bullhorn_24dp.xml new file mode 100644 index 0000000..e290b24 --- /dev/null +++ b/app/src/main/res/drawable/ic_bullhorn_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#FF000000" + android:pathData="M12,8H4A2,2 0,0 0,2 10V14A2,2 0,0 0,4 16H5V20A1,1 0,0 0,6 21H8A1,1 0,0 0,9 20V16H12L17,20V4L12,8M21.5,12C21.5,13.71 20.54,15.26 19,16V8C20.53,8.75 21.5,10.3 21.5,12Z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_cancel_24dp.xml b/app/src/main/res/drawable/ic_cancel_24dp.xml new file mode 100644 index 0000000..7d2b57e --- /dev/null +++ b/app/src/main/res/drawable/ic_cancel_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M12,2C6.47,2 2,6.47 2,12s4.47,10 10,10 10,-4.47 10,-10S17.53,2 12,2zM17,15.59L15.59,17 12,13.41 8.41,17 7,15.59 10.59,12 7,8.41 8.41,7 12,10.59 15.59,7 17,8.41 13.41,12 17,15.59z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_check_24dp.xml b/app/src/main/res/drawable/ic_check_24dp.xml new file mode 100644 index 0000000..6541ee3 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_check_32dp.xml b/app/src/main/res/drawable/ic_check_32dp.xml new file mode 100644 index 0000000..9325c89 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_32dp.xml @@ -0,0 +1,4 @@ +<vector android:height="32dp" android:viewportHeight="24.0" + android:viewportWidth="24.0" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="?attr/colorControlNormal" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_check_box_outline_blank_18dp.xml b/app/src/main/res/drawable/ic_check_box_outline_blank_18dp.xml new file mode 100644 index 0000000..cb610e2 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_box_outline_blank_18dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="18dp" + android:height="18dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#FF000000" + android:pathData="M19,5v14H5V5h14m0,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_check_circle.xml b/app/src/main/res/drawable/ic_check_circle.xml new file mode 100644 index 0000000..6024550 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_circle.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="18dp" + android:width="18dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="?attr/colorPrimary" android:pathData="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_clear_24dp.xml b/app/src/main/res/drawable/ic_clear_24dp.xml new file mode 100644 index 0000000..0a244b9 --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#fff" + android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_close_24dp.xml b/app/src/main/res/drawable/ic_close_24dp.xml new file mode 100644 index 0000000..081e405 --- /dev/null +++ b/app/src/main/res/drawable/ic_close_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="?android:attr/textColorSecondary" + android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_content_copy_24.xml b/app/src/main/res/drawable/ic_content_copy_24.xml new file mode 100644 index 0000000..bac0f60 --- /dev/null +++ b/app/src/main/res/drawable/ic_content_copy_24.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#000000" + 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="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_create_24dp.xml b/app/src/main/res/drawable/ic_create_24dp.xml new file mode 100644 index 0000000..d74fe13 --- /dev/null +++ b/app/src/main/res/drawable/ic_create_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="@color/white" + android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_cw_24dp.xml b/app/src/main/res/drawable/ic_cw_24dp.xml new file mode 100644 index 0000000..62713d7 --- /dev/null +++ b/app/src/main/res/drawable/ic_cw_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#FF000000" + android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM13,14h-2v-2h2v2zM13,10h-2L11,6h2v4z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_drag_indicator_24dp.xml b/app/src/main/res/drawable/ic_drag_indicator_24dp.xml new file mode 100644 index 0000000..ab9d5f3 --- /dev/null +++ b/app/src/main/res/drawable/ic_drag_indicator_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="?android:attr/textColorPrimary" + android:pathData="M11,18c0,1.1 -0.9,2 -2,2s-2,-0.9 -2,-2 0.9,-2 2,-2 2,0.9 2,2zM9,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM9,4c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM15,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM15,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM15,16c-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_drag_indicator_horiz_24dp.xml b/app/src/main/res/drawable/ic_drag_indicator_horiz_24dp.xml new file mode 100644 index 0000000..e8be67f --- /dev/null +++ b/app/src/main/res/drawable/ic_drag_indicator_horiz_24dp.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape android:shape="rectangle" + xmlns:android="http://schemas.android.com/apk/res/android"> + <size + android:width="8dp" + android:height="1dp" /> + <solid android:color="?android:textColorTertiary" /> + <corners android:radius="1dp" /> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_edit_24dp.xml b/app/src/main/res/drawable/ic_edit_24dp.xml new file mode 100644 index 0000000..2844baf --- /dev/null +++ b/app/src/main/res/drawable/ic_edit_24dp.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_email_24dp.xml b/app/src/main/res/drawable/ic_email_24dp.xml new file mode 100644 index 0000000..1bcee1b --- /dev/null +++ b/app/src/main/res/drawable/ic_email_24dp.xml @@ -0,0 +1,7 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="#fff" android:pathData="M4,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4C2.89,20 2,19.1 2,18V6C2,4.89 2.89,4 4,4M12,11L20,6H4L12,11M4,18H20V8.37L12,13.36L4,8.37V18Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_emoji_24dp.xml b/app/src/main/res/drawable/ic_emoji_24dp.xml new file mode 100644 index 0000000..5a73c89 --- /dev/null +++ b/app/src/main/res/drawable/ic_emoji_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <path + android:fillColor="#fff" + android:pathData="M12,17.5C14.33,17.5 16.3,16.04 17.11,14H6.89C7.69,16.04 9.67,17.5 12,17.5M8.5,11A1.5,1.5 0 0,0 10,9.5A1.5,1.5 0 0,0 8.5,8A1.5,1.5 0 0,0 7,9.5A1.5,1.5 0 0,0 8.5,11M15.5,11A1.5,1.5 0 0,0 17,9.5A1.5,1.5 0 0,0 15.5,8A1.5,1.5 0 0,0 14,9.5A1.5,1.5 0 0,0 15.5,11M12,20A8,8 0 0,1 4,12A8,8 0 0,1 12,4A8,8 0 0,1 20,12A8,8 0 0,1 12,20M12,2C6.47,2 2,6.5 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_eye_24dp.xml b/app/src/main/res/drawable/ic_eye_24dp.xml new file mode 100644 index 0000000..83a3463 --- /dev/null +++ b/app/src/main/res/drawable/ic_eye_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + <path + android:fillColor="?attr/colorControlNormal" + android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-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_favourite_24dp.xml b/app/src/main/res/drawable/ic_favourite_24dp.xml new file mode 100644 index 0000000..5826bf5 --- /dev/null +++ b/app/src/main/res/drawable/ic_favourite_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="?android:attr/textColorTertiary" + android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_favourite_active_24dp.xml b/app/src/main/res/drawable/ic_favourite_active_24dp.xml new file mode 100644 index 0000000..2eb3014 --- /dev/null +++ b/app/src/main/res/drawable/ic_favourite_active_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="@color/favoriteButtonActiveColor" + android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_file_download_black_24dp.xml b/app/src/main/res/drawable/ic_file_download_black_24dp.xml new file mode 100644 index 0000000..f5f7221 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_download_black_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFF" + android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_filter_24dp.xml b/app/src/main/res/drawable/ic_filter_24dp.xml new file mode 100644 index 0000000..ccb8fd2 --- /dev/null +++ b/app/src/main/res/drawable/ic_filter_24dp.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M4.25,5.61C6.27,8.2 10,13 10,13v6c0,0.55 0.45,1 1,1h2c0.55,0 1,-0.45 1,-1v-6c0,0 3.72,-4.8 5.74,-7.39C20.25,4.95 19.78,4 18.95,4H5.04C4.21,4 3.74,4.95 4.25,5.61z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_flag_24dp.xml b/app/src/main/res/drawable/ic_flag_24dp.xml new file mode 100644 index 0000000..03df926 --- /dev/null +++ b/app/src/main/res/drawable/ic_flag_24dp.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorPrimary"> + <path + android:fillColor="@android:color/white" + android:pathData="M14.4,6L14,4H5v17h2v-7h5.6l0.4,2h7V6z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_hashtag.xml b/app/src/main/res/drawable/ic_hashtag.xml new file mode 100644 index 0000000..c7a3bc0 --- /dev/null +++ b/app/src/main/res/drawable/ic_hashtag.xml @@ -0,0 +1,7 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="#000" android:pathData="M5.41,21L6.12,17H2.12L2.47,15H6.47L7.53,9H3.53L3.88,7H7.88L8.59,3H10.59L9.88,7H15.88L16.59,3H18.59L17.88,7H21.88L21.53,9H17.53L16.47,15H20.47L20.12,17H16.12L15.41,21H13.41L14.12,17H8.12L7.41,21H5.41M9.53,9L8.47,15H14.47L15.53,9H9.53Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_hide_media_24dp.xml b/app/src/main/res/drawable/ic_hide_media_24dp.xml new file mode 100644 index 0000000..106a53d --- /dev/null +++ b/app/src/main/res/drawable/ic_hide_media_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <path + android:fillColor="?attr/colorControlNormal" + android:pathData="M11.83,9L15,12.16C15,12.11 15,12.05 15,12A3,3 0 0,0 12,9C11.94,9 11.89,9 11.83,9M7.53,9.8L9.08,11.35C9.03,11.56 9,11.77 9,12A3,3 0 0,0 12,15C12.22,15 12.44,14.97 12.65,14.92L14.2,16.47C13.53,16.8 12.79,17 12,17A5,5 0 0,1 7,12C7,11.21 7.2,10.47 7.53,9.8M2,4.27L4.28,6.55L4.73,7C3.08,8.3 1.78,10 1,12C2.73,16.39 7,19.5 12,19.5C13.55,19.5 15.03,19.2 16.38,18.66L16.81,19.08L19.73,22L21,20.73L3.27,3M12,7A5,5 0 0,1 17,12C17,12.64 16.87,13.26 16.64,13.82L19.57,16.75C21.07,15.5 22.27,13.86 23,12C21.27,7.61 17,4.5 12,4.5C10.6,4.5 9.26,4.75 8,5.2L10.17,7.35C10.74,7.13 11.35,7 12,7Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_home_24dp.xml b/app/src/main/res/drawable/ic_home_24dp.xml new file mode 100644 index 0000000..4c6bc0e --- /dev/null +++ b/app/src/main/res/drawable/ic_home_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#fff" + android:pathData="M10,20v-6h4v6h5v-8h3L12,3 2,12h3v8z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_hot_24dp.xml b/app/src/main/res/drawable/ic_hot_24dp.xml new file mode 100644 index 0000000..9d4e664 --- /dev/null +++ b/app/src/main/res/drawable/ic_hot_24dp.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:tint="?attr/colorControlNormal"> + <path + android:fillColor="@android:color/white" + android:pathData="M240,560Q240,612 261,658.5Q282,705 321,740Q320,735 320,731Q320,727 320,722Q320,690 332,662Q344,634 367,611L480,500L593,611Q616,634 628,662Q640,690 640,722Q640,727 640,731Q640,735 639,740Q678,705 699,658.5Q720,612 720,560Q720,510 701.5,465.5Q683,421 648,386L648,386Q628,399 606,405.5Q584,412 561,412Q499,412 453.5,371Q408,330 401,270L401,270Q362,303 332,338.5Q302,374 281.5,410.5Q261,447 250.5,485Q240,523 240,560ZM480,612L423,668Q412,679 406,693Q400,707 400,722Q400,754 423.5,777Q447,800 480,800Q513,800 536.5,777Q560,754 560,722Q560,706 554,692.5Q548,679 537,668L480,612ZM480,120L480,252Q480,286 503.5,309Q527,332 561,332Q579,332 594.5,324.5Q610,317 622,302L640,280Q714,322 757,397Q800,472 800,560Q800,694 707,787Q614,880 480,880Q346,880 253,787Q160,694 160,560Q160,431 246.5,315Q333,199 480,120Z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..283975f --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + <group android:scaleX="0.1" + android:scaleY="0.1"> + <group> + + <path + android:pathData="m693.6,382.9c-2.3,-3.6 -1.4,-8.4 2,-11 9.2,-7.1 26.4,-20.1 35.9,-27.3 1.8,-1.4 4.1,-2 6.4,-1.6 2.3,0.4 4.3,1.6 5.5,3.5 6.3,9.5 11.3,20 14.7,31.2 0.6,2.2 0.3,4.6 -0.9,6.6 -1.2,2 -3.2,3.4 -5.4,3.8 -11.8,2.5 -33.1,6.9 -44.3,9.2 -4.1,0.8 -8.1,-1.4 -9.5,-5.3 -1.1,-3.3 -2.7,-6.3 -4.4,-9.2zM660.4,357.8c-4,-1.1 -6.7,-5 -6.1,-9.1 1.7,-12.9 5.3,-38.9 7.1,-52.4 0.3,-2.3 1.6,-4.3 3.4,-5.7 1.9,-1.3 4.2,-1.8 6.5,-1.4 12.5,2.7 24.5,7.2 35.5,13.4 2,1.1 3.4,3.1 3.9,5.3 0.5,2.2 0.1,4.6 -1.1,6.5 -7.4,11.4 -21.8,33.3 -29,44.2 -2.3,3.5 -6.9,4.8 -10.7,2.9 -3,-1.5 -6.2,-2.8 -9.5,-3.7zM618.6,362.7c-3.6,2 -8.1,1.1 -10.6,-2.2 -7,-9.1 -20.1,-26.4 -27.4,-36 -1.4,-1.8 -2,-4.2 -1.6,-6.5 0.4,-2.3 1.7,-4.3 3.7,-5.6 9.7,-6.1 20.5,-10.9 32,-14 2.2,-0.6 4.5,-0.2 6.5,1 1.9,1.2 3.3,3.1 3.7,5.4 2.5,11.7 6.8,32.8 9.2,44.2 0.9,4.2 -1.6,8.4 -5.7,9.7 -3.4,1 -6.7,2.4 -9.8,4.1z" + android:fillColor="@color/icon_highlight"/> + <path + android:pathData="m430.4,436.1c37.8,-14.9 80.9,-16.1 120.7,0.4 50.3,20.9 59.9,72.2 66.3,83.4 6.9,12 15.4,19.6 26.8,14.9 13.3,-5.5 9.6,-22.8 1.1,-37.7 -11.6,-20.4 -37.9,-41.8 -31.7,-71.4 3,-14.2 6.9,-24.3 9.8,-30.5 0.9,-1.6 2.7,-2.7 4.5,-2.7 1.9,-0 3.6,1 4.6,2.6 2.9,4.8 6.3,10.7 8.5,14.4 1.3,2.1 3.8,3.2 6.2,2.5 4.2,-1.1 10.9,-2.9 16.8,-4.5 2,-0.5 4.1,0.2 5.5,1.8 1.3,1.6 1.6,3.8 0.7,5.7 -2.8,5.7 -5.6,12.7 -5.6,17.8 0.1,31.9 24.6,26.8 49.8,56.4 25.2,29.6 55.9,85.4 10.6,140.1 -33.4,40.4 -76.3,50.7 -88.4,69.8 -12.1,19.2 -25.2,54.3 -7.1,94.3 18.1,40 43.9,102.5 46,155.1 0.8,20 -2.3,34.2 -4.9,42.5 -1,3 -3.1,5.8 -5.8,7.5 0.1,-0.6 -33.9,-6.6 -36.5,2h-6.6c0.1,-0.6 -33.3,-8.7 -35.9,0h-7.2c0,-0.3 -33.1,-8.6 -35.7,0h-6C534,987 525,964 518.3,953.5c-7.4,4.3 -15.7,7.7 -25,9.8 -7.4,1.6 -14.8,2.4 -22.1,2.5 -8.6,0.1 -28.1,29.2 -29.5,34.8l-3.9,-0c0.4,-1.1 -33.4,-8.7 -36,-0.1l-6.7,-0c0.1,-0.5 -33.2,-8.8 -35.8,-0.1l-7.2,-0c0,-0.1 -32.9,-8.7 -35.5,-0.1l-29.7,-0.1c-3.3,-0 -6.5,-1.1 -9.1,-3.2 -7.6,-6.2 -23.8,-21.5 -34.3,-47.4 -16.2,-0 -34.9,-2.6 -49.3,-11.9 -40.9,-26.5 -48.7,-63.1 -48.7,-63.1l4.3,-13.3 18.7,6.1c0,0 8.2,25.2 27.8,36.2 14.9,8.4 28.2,2.6 36.1,-2.9 -2.4,-30.2 3,-63.2 19.2,-100.2 14.8,-33.9 36.2,-57.4 57,-75.1 12.6,-0.3 20.8,-12.3 19.8,-15.5 -3,-23.4 -7.6,-65.3 -6.2,-90.3 2.3,-41.3 8,-88.7 36,-127.8 -5.3,-10 -10.6,-21.3 -12,-28.3 -2.6,-12.7 2.5,-20.9 10,-22.2 4.6,-0.7 8.8,2 12.7,7.6 0,-7.5 0.6,-16 2.4,-22.5 4.1,-15.1 12,-23 17.9,-22.8 6.4,0.2 11.4,9 12.6,20.6 3.2,-5.4 7.9,-11.2 13.9,-12.4 11.7,-2.4 16,7.7 16,15.6 -0,2.5 -0.4,5.5 -1.1,8.7z" + android:fillColor="#9baec8"/> + <path + android:pathData="m518.7,950c1.3,-12.4 2.2,-25.2 2.2,-36.4 0,-34.4 -20.1,-59.1 -20.1,-59.1 -4.4,-5.3 -3.6,-13.2 1.8,-17.6 5.3,-4.4 13.2,-3.6 17.6,1.8 0,0 25.7,31.3 25.7,75 0,29 -5.8,67.9 -8.9,87L527,1000.6c-6.8,0 -14.2,-6.2 -13.2,-12.9l0,-0c1.6,-10.4 3.5,-23.8 4.9,-37.7zM441.7,1000.5c3.2,-12.5 14.3,-65.5 -14.1,-115.6 -5.6,-9.9 -11.8,-16.2 -18.4,-19.8 -9.9,-5.3 -20.4,-4 -29.4,-0.9 -15.6,5.3 -27.3,16 -27.3,16v0c-5.1,4.7 -13,4.3 -17.7,-0.8 -4.7,-5.1 -4.3,-13 0.8,-17.7 0,0 15.5,-14.3 36.1,-21.3 15.1,-5.1 32.7,-6.3 49.3,2.6 10,5.4 19.8,14.5 28.3,29.5 18.3,32.1 22.5,65.3 22.1,91.1l-1.2,18.9 -0,0c-0.6,5.1 -1.9,8.9 -5.1,12.1 -3.2,3.2 -7.6,5.8 -12.2,5.8 -3.2,-0 -6.9,-0 -11.1,-0zM328.2,709.7c1,3.3 5.1,4 8.6,4.3 2.5,0.2 5.1,-0.2 7.3,-1.2 0.9,-0.4 1.7,-0.9 2,-1.8 0.1,-0.2 0.2,-0.5 0.2,-0.7 -0.5,-2.8 -0.7,-4.6 -0.7,-4.6 -0.6,-6.9 4.5,-12.9 11.4,-13.5 6.9,-0.6 12.9,4.5 13.5,11.4 0,0 1.1,18.6 11.1,28 6.2,5.7 12.4,3.4 16.5,0.9 0.3,-0.2 0.7,-0.4 1,-0.6 -0.7,-2.1 -1.3,-4.2 -1.8,-6.5 -1.6,-6.7 2.6,-13.4 9.4,-15 6.7,-1.6 13.4,2.6 15,9.4 2,8.5 5.4,14.1 10.1,16.6 5.5,2.9 12,0.9 15.8,-3.5 6.2,-7.2 5.7,-19.4 4.3,-31.3 -2.5,-21.8 -10.8,-43.1 -10.8,-43.1 -2.5,-6.4 0.7,-13.7 7.1,-16.2 6.4,-2.5 13.7,0.7 16.2,7.1 0,0 12.8,33.4 13.1,62.2 0.1,14.8 -3.2,28.3 -11,37.5 -11.2,13.1 -30.5,17.9 -46.6,9.3 -2.7,-1.4 -5.3,-3.2 -7.8,-5.5 -11.1,7.2 -29.5,13.9 -47.6,-2.9 -4.4,-4.1 -7.8,-9.2 -10.4,-14.5 -5.8,2.7 -12.4,3.9 -19.2,3.4 -10.7,-0.7 -20.8,-5.5 -26.7,-13.7 6.7,-5.7 13.4,-10.9 19.8,-15.5z" + android:fillColor="#5c6d82"/> + <path + android:pathData="m134.8,850.2c-2.5,-3.2 -4.1,-7.3 -3.2,-12.3 1.9,-10.5 15.1,-13.9 20.9,-7.6 2.7,3 4.2,6.7 5,10.3 2.5,-4.2 6.6,-8.3 13.1,-8.5 11.1,-0.4 15.4,13 10.1,22.5 -4.5,8 -12.3,12.7 -12.3,12.7l-23,7.2c-14,2.7 -30.9,-1.7 -27.2,-17.6 1.8,-7.8 9.2,-8.5 16.6,-6.7z" + android:fillColor="#5c6e83"/> + <path + android:pathData="m581.9,650.5c3.6,-2 8,-2.4 11.9,-1 9.3,3.3 31,8.9 53.5,-0.3 29.7,-12.2 51.3,-50.9 51.3,-50.9 0,0 10.2,24.4 4.1,46.4 -6.1,22 -32.6,62.1 -71.3,67 -38.7,4.9 -58.5,-7.8 -63.3,-27.8 -4,-16.2 4,-27.9 14,-33.5z" + android:fillColor="#ffffff"/> + <path + android:fillColor="#FF000000" + android:pathData="m548.5,568c-3.6,-1.9 -7.4,-6.4 -9.9,-11.5 -3.1,-6.4 -4,-14.5 4.1,-19.8 5.6,-3.7 11.8,-1.7 17.1,1.6 6.9,4.2 12.7,11 14.8,17.1 2.3,6.5 9.4,10 15.9,7.7 6.5,-2.3 10,-9.4 7.7,-15.9 -4.5,-12.8 -17.7,-27.8 -33,-34.1 -11.8,-4.8 -24.7,-4.9 -36.3,2.7 -21.2,14 -21,34.8 -12.8,51.7 5.1,10.3 13.4,18.8 20.7,22.6 6.1,3.2 13.7,0.9 16.9,-5.2 3.2,-6.1 0.9,-13.7 -5.2,-16.9z"/> + </group> + </group> +</vector> diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 0000000..fe445e7 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="108dp" + android:viewportHeight="1500" + android:viewportWidth="1500" + android:width="108dp"> + <path + android:fillColor="#FF000000" + android:pathData="m857.9,551.2c-16.8,0.1 -32.2,9.3 -40.5,23.9 -0.5,0.8 -0.9,1.7 -1.3,2.5 -4.7,10.1 -11.1,26.1 -15.8,48.7 -8.3,-5.4 -17.4,-10.3 -27.5,-14.5 -47.2,-19.6 -97.3,-23.3 -144.5,-13.8 -2.6,-3.6 -5.6,-6.9 -9,-9.7 -10.5,-8.8 -24.7,-14.8 -43.6,-12 -8.9,-7 -18.9,-10.5 -29.3,-10.8 -9.8,-0.4 -20.9,2.2 -31.8,9.7 -10.3,7.1 -22.3,21 -29.7,42.9 -23.7,8.5 -43.8,33.1 -35.5,73.8 1.4,6.9 4.9,16.4 9.3,26.4 -30.4,53.3 -38.4,113.9 -41.4,167.8 -1.8,32.9 4,87.1 7.7,120.3 4.1,35.7 27,62.6 62.6,66.4 6,0.6 11.6,0.5 16.8,-0.4 9.8,9.7 22.6,16.1 38.5,17.8 14.7,1.6 26.5,-1 36.3,-5.5 8.2,4.4 17.7,6.9 28.7,7.1 2.5,0 4.9,-0 7.2,-0.2 1.6,0.2 3.3,0.2 5,0 8.6,-1 23.3,-3.3 33.7,-12 22,8.9 52.6,18.8 92.9,26.6 31.3,6 57.6,6.9 78.2,5.8 32.8,-1.8 59.7,-26.6 64.2,-59.1 0,-0.1 0,-0.2 0,-0.2 2.1,-15.9 8.3,-29.3 14.1,-38.6 1.5,-2.3 4.4,-3.7 7.4,-5.8 7.6,-5.3 16.9,-10.6 26.9,-16.7 24.7,-15.1 53.2,-34.4 78.3,-64.8 76,-91.7 28.1,-185.4 -14.2,-235 -12.7,-15 -25.3,-24.7 -36.8,-32.4 -4.4,-2.9 -8.5,-5.5 -12.4,-8 -2,-1.3 -4.9,-3.5 -6.4,-4.6l-0,-0.4 -0.1,-0.8c0.6,-1.7 2.1,-5.5 3.1,-7.5 0,-0 0,-0.1 0,-0.1 8.1,-16.5 5.8,-36.2 -5.9,-50.4 -11.2,-13.7 -29.1,-19.8 -46.3,-16 -8.7,-12.9 -23.4,-20.7 -39.2,-20.6zM858.2,591.2c2.5,-0 4.8,1.3 6,3.4 3.7,6.3 8.3,14 11.1,18.9 1.6,2.8 5,4.2 8.1,3.3 5.5,-1.5 14.2,-3.8 21.9,-5.9 2.6,-0.7 5.4,0.2 7.2,2.3 1.7,2.1 2.1,5 0.9,7.5 -3.7,7.5 -7.3,16.6 -7.3,23.4 0.1,41.7 32.3,35.1 65.3,73.9 33,38.8 73.2,111.9 13.8,183.6 -10.8,13.1 -22.4,23.7 -33.9,32.7 2,-4.5 3.5,-8.7 4.6,-12.6 8,-28.8 -5.3,-60.8 -5.3,-60.8 0,0 -28.3,50.7 -67.2,66.7 -30.6,12.6 -60.3,4 -71.5,-0.2 -3.6,-1.4 -7.5,-1.3 -11,0.3 -14.3,6.5 -26.9,22.6 -21.3,45.4 6.3,25.8 31.6,42.4 81,36.6 -4.9,10.8 -9.1,23.7 -11,38 -1.9,13.6 -13.1,23.9 -26.8,24.7 -18,1 -41,0.1 -68.5,-5.1 -36.1,-6.9 -63.4,-15.6 -83,-23.4 0.7,-4.1 1.1,-8.4 1.3,-12.8 0.9,-20.9 -3.5,-44.7 -7.9,-62.5 -2.7,-10.7 -13.5,-17.2 -24.3,-14.5 -10.7,2.7 -17.2,13.5 -14.5,24.3 3.6,14.6 7.5,34 6.8,51.1 -0.1,1.4 -0.1,2.8 -0.2,4.2 -1.5,8.1 -6.7,18.8 -23.8,18.5 -25.4,-0.4 -28.4,-37.6 -28.7,-43.5 0.2,6.1 0.2,45.7 -32.6,42.2 -28.6,-3.1 -29,-46.9 -28.5,-59.7 -0.8,11 -5,44.6 -26.8,42.2 -16.2,-1.7 -25.3,-15.1 -27.2,-31.2 -3.6,-31.3 -9.3,-82.5 -7.6,-113.6 3,-54.1 10.5,-116.2 47.2,-167.4 -6.9,-13.1 -13.9,-27.8 -15.8,-37.1 -3.4,-16.6 3.2,-27.4 13.1,-29 6,-1 11.6,2.6 16.7,10 0.1,-9.8 0.8,-20.9 3.1,-29.5 5.3,-19.8 15.7,-30.2 23.5,-29.9 8.4,0.3 14.9,11.8 16.5,27 4.2,-7.1 10.4,-14.7 18.2,-16.3 15.3,-3.1 21,10 21,20.5 -0,3.3 -0.6,7.3 -1.5,11.5 49.6,-19.6 106,-21.2 158.2,0.5 65.9,27.4 78.4,94.6 86.8,109.2 9,15.8 20.2,25.7 35.1,19.5 17.5,-7.2 12.6,-29.8 1.5,-49.3 -15.2,-26.7 -49.6,-54.7 -41.5,-93.5 3.9,-18.6 9,-31.8 12.9,-40 1.2,-2.1 3.5,-3.5 5.9,-3.5zM754.5,820.1c-4.6,-2.4 -9.4,-8.1 -12.5,-14.6 -3.9,-7.9 -5,-17.9 4.9,-24.5 6.9,-4.5 14.6,-1.9 21.2,2.1 8.8,5.4 16.2,14 18.9,21.8 3.2,9.1 13.2,13.9 22.3,10.8 9.1,-3.2 13.9,-13.2 10.8,-22.3 -5.9,-17 -23.5,-36.9 -43.9,-45.3 -15.8,-6.5 -33,-6.6 -48.6,3.7 -28.3,18.7 -28.1,46.6 -17.1,69.1 6.7,13.8 17.9,25 27.6,30.1 8.5,4.5 19.1,1.3 23.7,-7.3 4.5,-8.5 1.3,-19.1 -7.3,-23.7zM969.9,551.6c-4,-6.6 -2.3,-15.2 3.8,-19.9 16.2,-12.3 45.2,-34.3 61.8,-46.8 3.3,-2.5 7.6,-3.6 11.7,-2.9 4.1,0.7 7.8,3.1 10.1,6.6 10.7,16.1 19.2,33.9 25,53.1 1.2,4.1 0.7,8.5 -1.5,12.2 -2.2,3.7 -5.8,6.3 -10,7.1 -20.6,4.3 -56.7,11.7 -76.2,15.8 -7.3,1.5 -14.6,-2.5 -17.3,-9.5 -2.1,-5.5 -4.6,-10.7 -7.6,-15.6zM838.8,515.8c-6.6,3.6 -14.8,1.8 -19.3,-4.2 -12.1,-15.9 -34.3,-45.2 -47,-62 -2.6,-3.4 -3.6,-7.7 -2.8,-11.9 0.8,-4.2 3.3,-7.9 6.9,-10.1 16.6,-10.4 34.8,-18.5 54.4,-23.9 4,-1.1 8.4,-0.5 11.9,1.7 3.6,2.2 6.1,5.8 6.9,9.9 4.2,20.3 11.6,56.1 15.7,76 1.6,7.6 -2.8,15.1 -10.2,17.5 -5.8,1.9 -11.3,4.2 -16.5,7.1zM912,507.4c-7.3,-2 -12,-9.2 -10.9,-16.7 3.1,-22.6 9.2,-66.8 12.4,-90.3 0.6,-4.2 2.9,-7.9 6.3,-10.4 3.5,-2.4 7.8,-3.3 11.9,-2.4 21.4,4.6 41.7,12.3 60.5,22.8 3.7,2.1 6.3,5.5 7.4,9.6 1,4.1 0.3,8.4 -2.1,11.9 -13,19.8 -37.5,57.1 -50,76.2 -4.2,6.4 -12.4,8.6 -19.3,5.3 -5.2,-2.5 -10.6,-4.5 -16.1,-6.1z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_link.xml b/app/src/main/res/drawable/ic_link.xml new file mode 100644 index 0000000..94455eb --- /dev/null +++ b/app/src/main/res/drawable/ic_link.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="?attr/colorPrimary" + android:pathData="M10.59,13.41C11,13.8 11,14.44 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C7.22,12.88 7.22,9.71 9.17,7.76V7.76L12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.59,9.17C9.41,10.34 9.41,12.24 10.59,13.41M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.78,11.12 16.78,14.29 14.83,16.24V16.24L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.65L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L13.41,14.83C14.59,13.66 14.59,11.76 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_list.xml b/app/src/main/res/drawable/ic_list.xml new file mode 100644 index 0000000..4c2fb88 --- /dev/null +++ b/app/src/main/res/drawable/ic_list.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FF000000" + android:pathData="M3,13h2v-2L3,11v2zM3,17h2v-2L3,15v2zM3,9h2L5,7L3,7v2zM7,13h14v-2L7,11v2zM7,17h14v-2L7,15v2zM7,7v2h14L21,7L7,7z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_local_24dp.xml b/app/src/main/res/drawable/ic_local_24dp.xml new file mode 100644 index 0000000..2953007 --- /dev/null +++ b/app/src/main/res/drawable/ic_local_24dp.xml @@ -0,0 +1,7 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="#000" android:pathData="M16,13C15.71,13 15.38,13 15.03,13.05C16.19,13.89 17,15 17,16.5V19H23V16.5C23,14.17 18.33,13 16,13M8,13C5.67,13 1,14.17 1,16.5V19H15V16.5C15,14.17 10.33,13 8,13M8,11A3,3 0 0,0 11,8A3,3 0 0,0 8,5A3,3 0 0,0 5,8A3,3 0 0,0 8,11M16,11A3,3 0 0,0 19,8A3,3 0 0,0 16,5A3,3 0 0,0 13,8A3,3 0 0,0 16,11Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_lock_open_24dp.xml b/app/src/main/res/drawable/ic_lock_open_24dp.xml new file mode 100644 index 0000000..1e9d0db --- /dev/null +++ b/app/src/main/res/drawable/ic_lock_open_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + <path + android:fillColor="#fff" + android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6h1.9c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM18,20L6,20L6,10h12v10z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_lock_outline_24dp.xml b/app/src/main/res/drawable/ic_lock_outline_24dp.xml new file mode 100644 index 0000000..a8e4201 --- /dev/null +++ b/app/src/main/res/drawable/ic_lock_outline_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + <path + android:fillColor="#fff" + android:pathData="M12,17c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1s3.1,1.39 3.1,3.1v2L8.9,8L8.9,6zM18,20L6,20L6,10h12v10z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_logout.xml b/app/src/main/res/drawable/ic_logout.xml new file mode 100644 index 0000000..717009a --- /dev/null +++ b/app/src/main/res/drawable/ic_logout.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#000" + android:pathData="M14.08,15.59L16.67,13H7V11H16.67L14.08,8.41L15.5,7L20.5,12L15.5,17L14.08,15.59M19,3A2,2 0 0,1 21,5V9.67L19,7.67V5H5V19H19V16.33L21,14.33V19A2,2 0 0,1 19,21H5C3.89,21 3,20.1 3,19V5C3,3.89 3.89,3 5,3H19Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_menu_share_24dp.xml b/app/src/main/res/drawable/ic_menu_share_24dp.xml new file mode 100644 index 0000000..dd1be97 --- /dev/null +++ b/app/src/main/res/drawable/ic_menu_share_24dp.xml @@ -0,0 +1,25 @@ +<!-- +Copyright (C) 2014 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + > + <path + android:pathData="M18,16.1c-0.8,0 -1.5,0.3 -2,0.8l-7.1,-4.2C9,12.5 9,12.2 9,12s0,-0.5 -0.1,-0.7L16,7.2C16.5,7.7 17.200001,8 18,8c1.7,0 3,-1.3 3,-3s-1.3,-3 -3,-3s-3,1.3 -3,3c0,0.2 0,0.5 0.1,0.7L8,9.8C7.5,9.3 6.8,9 6,9c-1.7,0 -2.9,1.2 -2.9,2.9s1.3,3 3,3c0.8,0 1.5,-0.3 2,-0.8l7.1,4.2c-0.1,0.3 -0.1,0.5 -0.1,0.7c0,1.6 1.3,2.9 2.9,2.9s2.9,-1.3 2.9,-2.9S19.6,16.1 18,16.1z" + android:fillColor="#FFF"/> +</vector> diff --git a/app/src/main/res/drawable/ic_missing_description_24dp.xml b/app/src/main/res/drawable/ic_missing_description_24dp.xml new file mode 100644 index 0000000..19d78d1 --- /dev/null +++ b/app/src/main/res/drawable/ic_missing_description_24dp.xml @@ -0,0 +1,5 @@ +<vector android:height="24dp" android:tint="#000000" + 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="M16.83,14H18v-2h-3.17l-1,-1H18V9h-6.17l-1,-1H18V6H8.83l-4,-4H20c1.1,0 2,0.9 2,2v15.17L16.83,14zM2.1,2.1L0.69,3.51L2,4.83V16c0,1.1 0.9,2 2,2h11.17l5.31,5.31l1.41,-1.41L2.1,2.1zM6,9h0.17l2,2H6V9zM6,14v-2h3.17l2,2H6z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_more_horiz_24dp.xml b/app/src/main/res/drawable/ic_more_horiz_24dp.xml new file mode 100644 index 0000000..c774133 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_horiz_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#fff" + 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_more_with_background.xml b/app/src/main/res/drawable/ic_more_with_background.xml new file mode 100644 index 0000000..72140f5 --- /dev/null +++ b/app/src/main/res/drawable/ic_more_with_background.xml @@ -0,0 +1,12 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32dp" + android:height="32dp" + android:viewportWidth="32" + android:viewportHeight="32"> + <path + android:fillColor="@color/toolbar_icon_background" + android:pathData="M16 0C7.152 0 0 7.152 0 16s7.152 16 16 16 16-7.152 16-16S24.848 0 16 0z" /> + <path + android:fillColor="?attr/colorControlNormal" + android:pathData="M16 12c1.1 0 2-0.9 2-2s-0.9-2-2-2-2 0.9-2 2 0.9 2 2 2zm0 2c-1.1 0-2 0.9-2 2s0.9 2 2 2 2-0.9 2-2-0.9-2-2-2zm0 6c-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_music_box_24dp.xml b/app/src/main/res/drawable/ic_music_box_24dp.xml new file mode 100644 index 0000000..c0243d9 --- /dev/null +++ b/app/src/main/res/drawable/ic_music_box_24dp.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="#000" android:pathData="M16,9H13V14.5A2.5,2.5 0 0,1 10.5,17A2.5,2.5 0 0,1 8,14.5A2.5,2.5 0 0,1 10.5,12C11.07,12 11.58,12.19 12,12.5V7H16M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_music_box_preview_24dp.xml b/app/src/main/res/drawable/ic_music_box_preview_24dp.xml new file mode 100644 index 0000000..6790179 --- /dev/null +++ b/app/src/main/res/drawable/ic_music_box_preview_24dp.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="?android:textColorTertiary" android:pathData="M16,9H13V14.5A2.5,2.5 0 0,1 10.5,17A2.5,2.5 0 0,1 8,14.5A2.5,2.5 0 0,1 10.5,12C11.07,12 11.58,12.19 12,12.5V7H16M19,3H5A2,2 0 0,0 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mute_24dp.xml b/app/src/main/res/drawable/ic_mute_24dp.xml new file mode 100644 index 0000000..bcdbb5a --- /dev/null +++ b/app/src/main/res/drawable/ic_mute_24dp.xml @@ -0,0 +1,12 @@ +<vector android:height="24dp" android:viewportHeight="35.43307" + android:viewportWidth="35.43307" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android" + android:autoMirrored="true"> + <path android:fillAlpha="1" android:fillColor="#ffffff" + android:pathData="m17.72,3.54 l-8.86,8.86 -7.09,0 0,10.63 7.09,0 8.86,8.86z" + android:strokeAlpha="1" android:strokeColor="#00000000" + android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="1.54400003"/> + <path android:fillAlpha="1" android:fillColor="#ffffff" + android:pathData="m22.86,11.45 l-2.51,2.51 3.76,3.76 -3.76,3.76 2.51,2.51 3.76,-3.76 3.76,3.76 2.5,-2.51 -3.76,-3.76 3.76,-3.76 -2.5,-2.51 -3.76,3.76 -3.76,-3.76z" + android:strokeAlpha="1" android:strokeColor="#00000000" + android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="1.54400003"/> +</vector> diff --git a/app/src/main/res/drawable/ic_notebook.xml b/app/src/main/res/drawable/ic_notebook.xml new file mode 100644 index 0000000..93ff789 --- /dev/null +++ b/app/src/main/res/drawable/ic_notebook.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="?attr/colorControlNormal" android:pathData="M3,7V5H5V4C5,2.89 5.9,2 7,2H13V9L15.5,7.5L18,9V2H19C20.05,2 21,2.95 21,4V20C21,21.05 20.05,22 19,22H7C5.95,22 5,21.05 5,20V19H3V17H5V13H3V11H5V7H3M7,11H5V13H7V11M7,7V5H5V7H7M7,19V17H5V19H7Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notifications_24dp.xml b/app/src/main/res/drawable/ic_notifications_24dp.xml new file mode 100644 index 0000000..d2f7aac --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#fff" + android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.89,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32L13.5,4c0,-0.83 -0.67,-1.5 -1.5,-1.5s-1.5,0.67 -1.5,1.5v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_notifications_active_24dp.xml b/app/src/main/res/drawable/ic_notifications_active_24dp.xml new file mode 100644 index 0000000..9a60daa --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_active_24dp.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + + <path + android:pathData="M0 0h24v24H0V0z" /> + <path + android:fillColor="#000000" + android:pathData="M18 16v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-0.83-0.68-1.5-1.51-1.5S10.5 3.17 10.5 4v0.68C7.63 5.36 6 7.92 6 11v5l-1.3 1.29c-0.63 0.63 -0.19 1.71 0.7 1.71h13.17c0.89 0 1.34-1.08 0.71 -1.71L18 16zm-6.01 6c1.1 0 2-0.9 2-2h-4c0 1.1 0.89 2 2 2zM6.77 4.73c0.42-0.38 0.43 -1.03 0.03 -1.43-0.38-0.38-1-0.39-1.39-0.02C3.7 4.84 2.52 6.96 2.14 9.34c-0.09 0.61 0.38 1.16 1 1.16 0.48 0 0.9-0.35 0.98 -0.83 0.3 -1.94 1.26-3.67 2.65-4.94zM18.6 3.28c-0.4-0.37-1.02-0.36-1.4 0.02 -0.4 0.4 -0.38 1.04 0.03 1.42 1.38 1.27 2.35 3 2.65 4.94 0.07 0.48 0.49 0.83 0.98 0.83 0.61 0 1.09-0.55 0.99 -1.16-0.38-2.37-1.55-4.48-3.25-6.05z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_notify.xml b/app/src/main/res/drawable/ic_notify.xml new file mode 100644 index 0000000..8baeb63 --- /dev/null +++ b/app/src/main/res/drawable/ic_notify.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:viewportHeight="460" + android:viewportWidth="460" + android:width="24dp"> + <path + android:fillColor="#FF000000" + android:pathData="m152.5,423.78c-10.93,-44.4 -37.22,-99.42 -23.82,-168.51 3.59,-18.53 15.96,-41.5 23.9,-54.91 1.43,-2.45 4.03,-3.99 6.88,-4.05 2.84,-0.06 5.51,1.35 7.05,3.74 6.06,9.28 14.52,22.38 19.49,30.08 2.42,3.75 7.04,5.44 11.32,4.14 8.27,-2.52 22.16,-6.76 33.52,-10.23 3.13,-0.95 6.52,0.04 8.65,2.52 2.13,2.48 2.58,5.99 1.15,8.93 -6.78,13.99 -13.69,30.78 -16.94,45.69 -10.53,48.28 41.55,67.22 63.85,119.5 0.43,1 0.84,2.01 1.23,3.01 -20.26,11.03 -42.78,18.71 -66.93,22.2 -23.75,3.43 -47.13,2.53 -69.36,-2.1zM271.94,182.43c-3.5,-6.07 -1.98,-13.78 3.56,-18.07 14.04,-10.86 39.05,-30.19 53.66,-41.49 3.09,-2.39 7.05,-3.38 10.9,-2.73 3.86,0.65 7.26,2.89 9.4,6.16 9.48,14.57 17,30.53 22.16,47.52 1.14,3.74 0.66,7.78 -1.32,11.15 -1.98,3.37 -5.28,5.76 -9.1,6.59 -18.06,3.93 -48.98,10.63 -66.32,14.38 -6.84,1.48 -13.72,-2.31 -16.11,-8.88 -1.84,-5.11 -4.13,-10 -6.82,-14.63zM173.02,125.96c1.36,6.86 -2.55,13.67 -9.16,15.96 -5.14,1.75 -10.07,3.96 -14.75,6.56 -6.13,3.39 -13.82,1.74 -18,-3.88 -10.61,-14.23 -29.51,-39.57 -40.55,-54.38 -2.34,-3.13 -3.26,-7.1 -2.54,-10.95 0.72,-3.84 3.01,-7.21 6.33,-9.29 14.72,-9.22 30.81,-16.46 47.9,-21.32 3.76,-1.07 7.79,-0.52 11.13,1.51 3.33,2.04 5.66,5.38 6.42,9.21 3.61,18.13 9.77,49.16 13.22,66.56zM217.45,140.31c-6.74,-1.88 -11.05,-8.45 -10.1,-15.38 2.78,-20.33 8.15,-59.61 11.05,-80.79 0.53,-3.86 2.64,-7.32 5.83,-9.56 3.19,-2.24 7.16,-3.05 10.97,-2.23 19.51,4.19 37.97,11.21 54.92,20.62 3.4,1.89 5.86,5.11 6.79,8.88 0.93,3.78 0.25,7.77 -1.89,11.02 -11.71,17.87 -33.46,51 -44.72,68.16 -3.84,5.86 -11.42,7.97 -17.74,4.95 -4.82,-2.32 -9.87,-4.22 -15.11,-5.67z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_person_add_24dp.xml b/app/src/main/res/drawable/ic_person_add_24dp.xml new file mode 100644 index 0000000..b42eb2f --- /dev/null +++ b/app/src/main/res/drawable/ic_person_add_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="?attr/colorPrimary" + android:pathData="M15,12c2.21,0 4,-1.79 4,-4s-1.79,-4 -4,-4 -4,1.79 -4,4 1.79,4 4,4zM6,10L6,7L4,7v3L1,10v2h3v3h2v-3h3v-2L6,10zM15,14c-2.67,0 -8,1.34 -8,4v2h16v-2c0,-2.66 -5.33,-4 -8,-4z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_person_remove_24dp.xml b/app/src/main/res/drawable/ic_person_remove_24dp.xml new file mode 100644 index 0000000..9da6463 --- /dev/null +++ b/app/src/main/res/drawable/ic_person_remove_24dp.xml @@ -0,0 +1,8 @@ +<!-- drawable/account_remove.xml --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="#000" android:pathData="M15,14C17.67,14 23,15.33 23,18V20H7V18C7,15.33 12.33,14 15,14M15,12A4,4 0 0,1 11,8A4,4 0 0,1 15,4A4,4 0 0,1 19,8A4,4 0 0,1 15,12M5,9.59L7.12,7.46L8.54,8.88L6.41,11L8.54,13.12L7.12,14.54L5,12.41L2.88,14.54L1.46,13.12L3.59,11L1.46,8.88L2.88,7.46L5,9.59Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_photo_24dp.xml b/app/src/main/res/drawable/ic_photo_24dp.xml new file mode 100644 index 0000000..d0ebff0 --- /dev/null +++ b/app/src/main/res/drawable/ic_photo_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z" + android:fillColor="#FFFFFF"/> +</vector> diff --git a/app/src/main/res/drawable/ic_play_indicator.xml b/app/src/main/res/drawable/ic_play_indicator.xml new file mode 100644 index 0000000..451e51e --- /dev/null +++ b/app/src/main/res/drawable/ic_play_indicator.xml @@ -0,0 +1,8 @@ +<vector android:height="48dp" android:viewportHeight="24.0" + android:viewportWidth="24.0" android:width="48dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillAlpha=".85" android:fillColor="?attr/colorSurface" + android:pathData="M21.282,12A9.282,9.282 0,0 1,12 21.282,9.282 9.282,0 0,1 2.718,12 9.282,9.282 0,0 1,12 2.718,9.282 9.282,0 0,1 21.282,12Z" + android:strokeAlpha="1" android:strokeColor="#00000000" + android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="8"/> + <path android:fillAlpha="1" android:fillColor="?attr/colorPrimary" android:pathData="M10,16.5l6,-4.5 -6,-4.5v9zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_plus_24dp.xml b/app/src/main/res/drawable/ic_plus_24dp.xml new file mode 100644 index 0000000..2ba0da8 --- /dev/null +++ b/app/src/main/res/drawable/ic_plus_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#fff" + android:pathData="M19,13H13V19H11V13H5V11H11V5H13V11H19V13Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_poll_24dp.xml b/app/src/main/res/drawable/ic_poll_24dp.xml new file mode 100644 index 0000000..dd0c4ab --- /dev/null +++ b/app/src/main/res/drawable/ic_poll_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="?attr/colorPrimary" + android:pathData="M19,3L5,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2L21,5c0,-1.1 -0.9,-2 -2,-2zM9,17L7,17v-7h2v7zM13,17h-2L11,7h2v10zM17,17h-2v-4h2v4z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_public_24dp.xml b/app/src/main/res/drawable/ic_public_24dp.xml new file mode 100644 index 0000000..6ef182e --- /dev/null +++ b/app/src/main/res/drawable/ic_public_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#fff" + android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM11,19.93c-3.95,-0.49 -7,-3.85 -7,-7.93 0,-0.62 0.08,-1.21 0.21,-1.79L9,15v1c0,1.1 0.9,2 2,2v1.93zM17.9,17.39c-0.26,-0.81 -1,-1.39 -1.9,-1.39h-1v-3c0,-0.55 -0.45,-1 -1,-1L8,12v-2h2c0.55,0 1,-0.45 1,-1L11,7h2c1.1,0 2,-0.9 2,-2v-0.41c2.93,1.19 5,4.06 5,7.41 0,2.08 -0.8,3.97 -2.1,5.39z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_quicksettings.xml b/app/src/main/res/drawable/ic_quicksettings.xml new file mode 100644 index 0000000..d3a4807 --- /dev/null +++ b/app/src/main/res/drawable/ic_quicksettings.xml @@ -0,0 +1,6 @@ +<vector android:height="24dp" android:viewportHeight="480" + android:viewportWidth="480" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> + <path android:fillColor="#000000" + android:pathData="M240,0A240,240 0,0 0,0 240A240,240 0,0 0,240 480A240,240 0,0 0,480 240A240,240 0,0 0,240 0zM261.77,69.69C262.51,69.69 263.25,69.78 263.99,69.96C279.07,73.56 293.35,79.6 306.45,87.7C309.08,89.32 310.99,92.09 311.71,95.34C312.42,98.59 311.9,102.02 310.25,104.82C301.19,120.19 284.38,148.7 275.67,163.46C272.7,168.5 266.84,170.32 261.95,167.72C258.23,165.72 254.32,164.08 250.27,162.84C245.06,161.22 241.72,155.57 242.46,149.61C244.6,132.11 248.76,98.32 251,80.1C251.41,76.78 253.04,73.8 255.5,71.88C257.35,70.43 259.55,69.68 261.77,69.69zM194.32,83.56C196.55,83.39 198.78,83.99 200.72,85.3C203.3,87.05 205.1,89.92 205.69,93.22C208.48,108.82 213.24,135.51 215.91,150.49C216.96,156.39 213.94,162.25 208.82,164.22C204.85,165.73 201.04,167.63 197.42,169.87C192.68,172.79 186.74,171.36 183.5,166.53C175.3,154.29 160.68,132.49 152.15,119.74C150.34,117.05 149.63,113.63 150.19,110.33C150.74,107.02 152.52,104.12 155.08,102.34C166.46,94.4 178.9,88.18 192.12,84C192.84,83.77 193.58,83.62 194.32,83.56zM342.83,145.33C343.58,145.3 344.33,145.35 345.07,145.49C348.05,146.05 350.69,147.98 352.34,150.79C359.67,163.33 365.48,177.06 369.47,191.67C370.35,194.89 369.98,198.37 368.45,201.27C366.92,204.17 364.37,206.22 361.41,206.94C347.44,210.32 323.54,216.08 310.13,219.31C304.85,220.58 299.53,217.32 297.67,211.67C296.25,207.27 294.48,203.06 292.4,199.08C289.69,193.86 290.87,187.22 295.15,183.54C306,174.19 325.34,157.56 336.64,147.84C338.44,146.29 340.6,145.43 342.83,145.33zM205.43,211.01C207.63,210.96 209.69,212.17 210.88,214.23C215.56,222.22 222.11,233.48 225.95,240.1C227.83,243.33 231.4,244.79 234.7,243.66C241.1,241.49 251.83,237.85 260.62,234.87C263.04,234.05 265.66,234.89 267.31,237.03C268.95,239.17 269.3,242.18 268.2,244.71C262.96,256.75 257.62,271.2 255.1,284.02C246.96,325.56 287.23,341.85 304.47,386.83C304.81,387.69 305.12,388.56 305.43,389.42C289.76,398.91 272.35,405.52 253.67,408.52C235.31,411.47 217.23,410.7 200.05,406.71C191.6,368.52 171.27,321.18 181.63,261.74C184.41,245.8 193.97,226.04 200.11,214.5C201.22,212.39 203.23,211.07 205.43,211.01z" + android:strokeColor="#ed0000" android:strokeWidth="0"/> +</vector> diff --git a/app/src/main/res/drawable/ic_radio_button_unchecked_18dp.xml b/app/src/main/res/drawable/ic_radio_button_unchecked_18dp.xml new file mode 100644 index 0000000..160b237 --- /dev/null +++ b/app/src/main/res/drawable/ic_radio_button_unchecked_18dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="18dp" + android:height="18dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="#FF000000" + android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_reblog_18dp.xml b/app/src/main/res/drawable/ic_reblog_18dp.xml new file mode 100644 index 0000000..029e711 --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_18dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="18dp" + android:height="18dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="?android:attr/textColorTertiary" + android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_reblog_24dp.xml b/app/src/main/res/drawable/ic_reblog_24dp.xml new file mode 100644 index 0000000..0fe908e --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="?android:attr/textColorTertiary" + android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_reblog_active_24dp.xml b/app/src/main/res/drawable/ic_reblog_active_24dp.xml new file mode 100644 index 0000000..48d987f --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_active_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:fillColor="?attr/colorPrimary" + android:pathData="M17,2L17,5L5,5L5,11L7,11L7,7L17,7L17,10L21,6L17,2zM9.75,9.75L9.75,14.25L14.25,14.25L14.25,9.75L9.75,9.75zM17,13L17,17L7,17L7,14L3,18L7,22L7,19L19,19L19,13L17,13z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_reblog_direct_24dp.xml b/app/src/main/res/drawable/ic_reblog_direct_24dp.xml new file mode 100644 index 0000000..0f53287 --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_direct_24dp.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="@color/textColorDisabled" + android:pathData="M20,4L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,6c0,-1.1 -0.9,-2 -2,-2zM20,8l-8,5 -8,-5L4,6l8,5 8,-5v2z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_reblog_private_24dp.xml b/app/src/main/res/drawable/ic_reblog_private_24dp.xml new file mode 100644 index 0000000..078eaf7 --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_private_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" +android:width="24dp" +android:height="24dp" +android:viewportWidth="24.0" +android:viewportHeight="24.0"> +<path + android:fillColor="@color/textColorDisabled" + android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_reblog_private_active_24dp.xml b/app/src/main/res/drawable/ic_reblog_private_active_24dp.xml new file mode 100644 index 0000000..48b7351 --- /dev/null +++ b/app/src/main/res/drawable/ic_reblog_private_active_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="?attr/colorPrimary" + android:pathData="M18,8h-1L17,6c0,-2.76 -2.24,-5 -5,-5S7,3.24 7,6v2L6,8c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2L20,10c0,-1.1 -0.9,-2 -2,-2zM12,17c-1.1,0 -2,-0.9 -2,-2s0.9,-2 2,-2 2,0.9 2,2 -0.9,2 -2,2zM15.1,8L8.9,8L8.9,6c0,-1.71 1.39,-3.1 3.1,-3.1 1.71,0 3.1,1.39 3.1,3.1v2z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_reject_24dp.xml b/app/src/main/res/drawable/ic_reject_24dp.xml new file mode 100644 index 0000000..d11cc5c --- /dev/null +++ b/app/src/main/res/drawable/ic_reject_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_repeat_24dp.xml b/app/src/main/res/drawable/ic_repeat_24dp.xml new file mode 100644 index 0000000..aaa76ae --- /dev/null +++ b/app/src/main/res/drawable/ic_repeat_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#fff" + android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_reply_24dp.xml b/app/src/main/res/drawable/ic_reply_24dp.xml new file mode 100644 index 0000000..6085ff0 --- /dev/null +++ b/app/src/main/res/drawable/ic_reply_24dp.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:autoMirrored="true" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + <path + android:fillColor="#fff" + android:pathData="M10,9V5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_reply_all_24dp.xml b/app/src/main/res/drawable/ic_reply_all_24dp.xml new file mode 100644 index 0000000..9da31f0 --- /dev/null +++ b/app/src/main/res/drawable/ic_reply_all_24dp.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:autoMirrored="true" + android:viewportHeight="24.0" + android:viewportWidth="24.0"> + <path + android:fillColor="#fff" + android:pathData="M7,8L7,5l-7,7 7,7v-3l-4,-4 4,-4zM13,9L13,5l-7,7 7,7v-4.1c5,0 8.5,1.6 11,5.1 -1,-5 -4,-10 -11,-11z" /> +</vector> diff --git a/app/src/main/res/drawable/ic_send_24dp.xml b/app/src/main/res/drawable/ic_send_24dp.xml new file mode 100644 index 0000000..8916aa9 --- /dev/null +++ b/app/src/main/res/drawable/ic_send_24dp.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0" + android:autoMirrored="true"> + <path + android:fillColor="#fff" + android:pathData="M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_splash.xml b/app/src/main/res/drawable/ic_splash.xml new file mode 100644 index 0000000..90d7ab9 --- /dev/null +++ b/app/src/main/res/drawable/ic_splash.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<layer-list xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_height="240dp" + android:layout_width="240dp"> + + <item android:drawable="@color/icon_background" /> + + <item + android:drawable="@drawable/ic_launcher_foreground" /> + +</layer-list> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_star_24dp.xml b/app/src/main/res/drawable/ic_star_24dp.xml new file mode 100644 index 0000000..8689142 --- /dev/null +++ b/app/src/main/res/drawable/ic_star_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:fillColor="#fff" + android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_tabs.xml b/app/src/main/res/drawable/ic_tabs.xml new file mode 100644 index 0000000..3de93e6 --- /dev/null +++ b/app/src/main/res/drawable/ic_tabs.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="#000" android:pathData="M22,14A2,2 0 0,1 20,16H4A2,2 0 0,1 2,14V10A2,2 0 0,1 4,8H20A2,2 0 0,1 22,10V14M4,14H8V10H4V14M10,14H14V10H10V14M16,14H20V10H16V14Z" /> +</vector> \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_trending_up_24px.xml b/app/src/main/res/drawable/ic_trending_up_24px.xml new file mode 100644 index 0000000..95e98c2 --- /dev/null +++ b/app/src/main/res/drawable/ic_trending_up_24px.xml @@ -0,0 +1,11 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24" + android:tint="?attr/colorControlNormal" + android:autoMirrored="true"> + <path + android:fillColor="@android:color/white" + android:pathData="M3.4,18 L2,16.6 9.4,9.15 13.4,13.15 18.6,8H16V6H22V12H20V9.4L13.4,16L9.4,12Z"/> +</vector> diff --git a/app/src/main/res/drawable/ic_unmute_24dp.xml b/app/src/main/res/drawable/ic_unmute_24dp.xml new file mode 100644 index 0000000..fe37f7f --- /dev/null +++ b/app/src/main/res/drawable/ic_unmute_24dp.xml @@ -0,0 +1,20 @@ +<vector android:height="24dp" android:viewportHeight="35.43307" + android:viewportWidth="35.43307" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android" + android:autoMirrored="true"> + <path android:fillAlpha="1" android:fillColor="#ffffff" + android:pathData="m17.72,3.54 l-8.86,8.86 -7.09,0 0,10.63 7.09,0 8.86,8.86z" + android:strokeAlpha="1" android:strokeColor="#00000000" + android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="1.54400003"/> + <path android:fillAlpha="1" android:fillColor="#00000000" + android:pathData="m21.47,13.96a5.31,5.31 0,0 1,1.56 3.76,5.31 5.31,0 0,1 -1.56,3.76" + android:strokeAlpha="1" android:strokeColor="#ffffff" + android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="2.65748031"/> + <path android:fillAlpha="1" android:fillColor="#00000000" + android:pathData="m28.99,6.44a15.94,15.94 0,0 1,4.67 11.27,15.94 15.94,0 0,1 -4.67,11.27" + android:strokeAlpha="1" android:strokeColor="#ffffff" + android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="2.65748031"/> + <path android:fillAlpha="1" android:fillColor="#00000000" + android:pathData="m25.23,10.2a10.63,10.63 0,0 1,3.11 7.52,10.63 10.63,0 0,1 -3.11,7.52" + android:strokeAlpha="1" android:strokeColor="#ffffff" + android:strokeLineCap="square" android:strokeLineJoin="miter" android:strokeWidth="2.65748031"/> +</vector> diff --git a/app/src/main/res/drawable/ic_videocam_24dp.xml b/app/src/main/res/drawable/ic_videocam_24dp.xml new file mode 100644 index 0000000..1614d02 --- /dev/null +++ b/app/src/main/res/drawable/ic_videocam_24dp.xml @@ -0,0 +1,9 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + <path + android:pathData="M17,10.5V7c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l4,4v-11l-4,4z" + android:fillColor="#FFFFFF"/> +</vector> diff --git a/app/src/main/res/drawable/info_24dp.xml b/app/src/main/res/drawable/info_24dp.xml new file mode 100644 index 0000000..0f7a508 --- /dev/null +++ b/app/src/main/res/drawable/info_24dp.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="48" + android:viewportHeight="48" + android:tint="?attr/colorPrimary"> + <path + android:fillColor="@android:color/white" + android:pathData="M22.65,34H25.65V22H22.65ZM24,18.3Q24.7,18.3 25.175,17.85Q25.65,17.4 25.65,16.7Q25.65,16 25.175,15.5Q24.7,15 24,15Q23.3,15 22.825,15.5Q22.35,16 22.35,16.7Q22.35,17.4 22.825,17.85Q23.3,18.3 24,18.3ZM24,44Q19.9,44 16.25,42.425Q12.6,40.85 9.875,38.125Q7.15,35.4 5.575,31.75Q4,28.1 4,23.95Q4,19.85 5.575,16.2Q7.15,12.55 9.875,9.85Q12.6,7.15 16.25,5.575Q19.9,4 24.05,4Q28.15,4 31.8,5.575Q35.45,7.15 38.15,9.85Q40.85,12.55 42.425,16.2Q44,19.85 44,24Q44,28.1 42.425,31.75Q40.85,35.4 38.15,38.125Q35.45,40.85 31.8,42.425Q28.15,44 24,44ZM24.05,41Q31.1,41 36.05,36.025Q41,31.05 41,23.95Q41,16.9 36.05,11.95Q31.1,7 24,7Q16.95,7 11.975,11.95Q7,16.9 7,24Q7,31.05 11.975,36.025Q16.95,41 24.05,41ZM24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Q24,24 24,24Z"/> +</vector> diff --git a/app/src/main/res/drawable/materialdrawer_shape_large.xml b/app/src/main/res/drawable/materialdrawer_shape_large.xml new file mode 100644 index 0000000..ba626b6 --- /dev/null +++ b/app/src/main/res/drawable/materialdrawer_shape_large.xml @@ -0,0 +1,5 @@ +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle" > + <solid android:color="#000"/> + <corners android:radius="7dp"/> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/materialdrawer_shape_small.xml b/app/src/main/res/drawable/materialdrawer_shape_small.xml new file mode 100644 index 0000000..7bdd429 --- /dev/null +++ b/app/src/main/res/drawable/materialdrawer_shape_small.xml @@ -0,0 +1,5 @@ +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle" > + <solid android:color="#000"/> + <corners android:radius="5dp"/> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/media_preview_outline.xml b/app/src/main/res/drawable/media_preview_outline.xml new file mode 100644 index 0000000..a15ba5c --- /dev/null +++ b/app/src/main/res/drawable/media_preview_outline.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="8dp" /> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/media_warning_bg.xml b/app/src/main/res/drawable/media_warning_bg.xml new file mode 100644 index 0000000..c1628a6 --- /dev/null +++ b/app/src/main/res/drawable/media_warning_bg.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="7dp" /> + <solid android:color="@color/color_background_transparent_60" /> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/poll_option_background.xml b/app/src/main/res/drawable/poll_option_background.xml new file mode 100644 index 0000000..90aa51d --- /dev/null +++ b/app/src/main/res/drawable/poll_option_background.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<clip + xmlns:android="http://schemas.android.com/apk/res/android" + android:drawable="@drawable/poll_option_shape" + android:clipOrientation="horizontal" + android:gravity="start|clip_horizontal|fill_vertical"/> \ No newline at end of file diff --git a/app/src/main/res/drawable/poll_option_shape.xml b/app/src/main/res/drawable/poll_option_shape.xml new file mode 100644 index 0000000..da097f3 --- /dev/null +++ b/app/src/main/res/drawable/poll_option_shape.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape + xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="6dp"/> + <solid android:color="?attr/colorBackgroundAccent" /> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/profile_badge_person_24dp.xml b/app/src/main/res/drawable/profile_badge_person_24dp.xml new file mode 100644 index 0000000..628c29d --- /dev/null +++ b/app/src/main/res/drawable/profile_badge_person_24dp.xml @@ -0,0 +1,10 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="m12,12c2.688,0 4.864,-2.177 4.864,-4.864 0,-2.688 -2.177,-4.864 -4.864,-4.864 -2.688,0 -4.864,2.177 -4.864,4.864C7.136,9.823 9.312,12 12,12ZM12,14.432c-3.247,0 -9.729,1.63 -9.729,4.864v2.432L21.729,21.729L21.729,19.297c0,-3.235 -6.482,-4.864 -9.729,-4.864z" + android:strokeWidth="1.2161" + android:fillColor="#000000"/> +</vector> diff --git a/app/src/main/res/drawable/report_success_background.xml b/app/src/main/res/drawable/report_success_background.xml new file mode 100644 index 0000000..f1685bc --- /dev/null +++ b/app/src/main/res/drawable/report_success_background.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval"> + <solid android:color="?attr/colorPrimary" /> +</shape> diff --git a/app/src/main/res/drawable/round_button.xml b/app/src/main/res/drawable/round_button.xml new file mode 100644 index 0000000..a6c0da1 --- /dev/null +++ b/app/src/main/res/drawable/round_button.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <corners android:radius="10dp" /> + <solid android:color="#66000000" /> + <size + android:width="80dp" + android:height="80dp"/> +</shape> \ No newline at end of file diff --git a/app/src/main/res/drawable/spellcheck.xml b/app/src/main/res/drawable/spellcheck.xml new file mode 100644 index 0000000..79f2251 --- /dev/null +++ b/app/src/main/res/drawable/spellcheck.xml @@ -0,0 +1,8 @@ +<!-- drawable/spellcheck.xml --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:height="24dp" + android:width="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path android:fillColor="#000" android:pathData="M21.59,11.59L13.5,19.68L9.83,16L8.42,17.41L13.5,22.5L23,13M6.43,11L8.5,5.5L10.57,11M12.45,16H14.54L9.43,3H7.57L2.46,16H4.55L5.67,13H11.31L12.45,16Z" /> +</vector> diff --git a/app/src/main/res/drawable/status_divider.xml b/app/src/main/res/drawable/status_divider.xml new file mode 100644 index 0000000..37fbbab --- /dev/null +++ b/app/src/main/res/drawable/status_divider.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <size android:height="1dp" /> + <solid android:color="?attr/dividerColor" /> +</shape> \ No newline at end of file diff --git a/app/src/main/res/layout-land/fragment_report_done.xml b/app/src/main/res/layout-land/fragment_report_done.xml new file mode 100644 index 0000000..6263758 --- /dev/null +++ b/app/src/main/res/layout-land/fragment_report_done.xml @@ -0,0 +1,109 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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" + tools:context=".components.report.fragments.ReportStatusesFragment"> + + <View + android:id="@+id/checkMark" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="48dp" + android:background="@drawable/report_success_background" + app:layout_constraintDimensionRatio="W,1:1" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintHeight_default="percent" + app:layout_constraintHeight_percent="0.4" /> + + <ImageView + android:layout_width="0dp" + android:layout_height="0dp" + android:scaleType="fitCenter" + app:layout_constraintBottom_toBottomOf="@id/checkMark" + app:layout_constraintEnd_toEndOf="@id/checkMark" + app:layout_constraintHeight_default="percent" + app:layout_constraintHeight_percent="0.25" + app:layout_constraintStart_toStartOf="@id/checkMark" + app:layout_constraintTop_toTopOf="@id/checkMark" + app:srcCompat="@drawable/ic_check_24dp" + tools:ignore="ContentDescription" /> + + <TextView + android:id="@+id/textReported" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="48dp" + android:gravity="center_horizontal" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_large" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/checkMark" + app:layout_constraintTop_toTopOf="parent" + android:layout_marginStart="48dp" + android:layout_marginEnd="16dp"/> + + <Button + android:id="@+id/buttonMute" + style="@style/TuskyButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="@dimen/min_report_button_width" + android:text="@string/action_mute" + app:layout_constraintBottom_toTopOf="@id/buttonBlock" + app:layout_constraintEnd_toEndOf="@id/textReported" + app:layout_constraintStart_toStartOf="@id/textReported" + app:layout_constraintTop_toBottomOf="@id/textReported" + app:layout_constraintVertical_chainStyle="packed" /> + + <ProgressBar + android:id="@+id/progressMute" + style="?android:attr/progressBarStyleSmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="@id/buttonMute" + app:layout_constraintEnd_toEndOf="@id/buttonMute" + app:layout_constraintStart_toStartOf="@id/buttonMute" + app:layout_constraintTop_toTopOf="@id/buttonMute" /> + + <Button + android:id="@+id/buttonBlock" + style="@style/TuskyButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="@dimen/min_report_button_width" + android:text="@string/action_block" + app:layout_constraintBottom_toTopOf="@id/buttonDone" + app:layout_constraintEnd_toEndOf="@id/textReported" + app:layout_constraintStart_toStartOf="@id/textReported" + app:layout_constraintTop_toBottomOf="@id/buttonMute" + app:layout_constraintVertical_chainStyle="packed" /> + + <ProgressBar + android:id="@+id/progressBlock" + style="?android:attr/progressBarStyleSmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="@id/buttonBlock" + app:layout_constraintEnd_toEndOf="@id/buttonBlock" + app:layout_constraintStart_toStartOf="@id/buttonBlock" + app:layout_constraintTop_toTopOf="@id/buttonBlock" /> + + <Button + android:id="@+id/buttonDone" + style="@style/TuskyButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + android:minWidth="@dimen/min_report_button_width" + android:text="@string/button_done" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="@id/textReported" + app:layout_constraintStart_toStartOf="@id/textReported" + app:layout_constraintTop_toBottomOf="@id/buttonBlock" + app:layout_constraintVertical_chainStyle="packed" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout-land/item_trending_cell.xml b/app/src/main/res/layout-land/item_trending_cell.xml new file mode 100644 index 0000000..4d5b030 --- /dev/null +++ b/app/src/main/res/layout-land/item_trending_cell.xml @@ -0,0 +1,162 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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/trending_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:clipToPadding="false" + android:focusable="true" + android:importantForAccessibility="yes" + android:padding="8dp" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + tools:layout_height="128dp"> + + <com.keylesspalace.tusky.view.GraphView + android:id="@+id/graph" + android:layout_width="0dp" + android:layout_height="120dp" + android:importantForAccessibility="no" + app:graphColor="?android:colorBackground" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/current_usage" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:lineWidth="2sp" + app:metaColor="?android:attr/textColorTertiary" + app:primaryLineColor="?attr/colorPrimary" + app:proportionalTrending="true" + app:secondaryLineColor="@color/warning_color" /> + + <TextView + android:id="@+id/current_usage" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + android:paddingStart="6dp" + android:textAlignment="textEnd" + android:textAppearance="@style/TextAppearance.AppCompat.Small" + android:textColor="?attr/colorPrimary" + android:textSize="8sp" + android:textStyle="normal" + app:layout_constrainedWidth="true" + app:layout_constraintBottom_toTopOf="@id/current_accounts" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/graph" + tools:text="12 345" /> + + <TextView + android:id="@+id/current_accounts" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + android:paddingStart="6dp" + android:textAlignment="textEnd" + android:textAppearance="@style/TextAppearance.AppCompat.Small" + android:textColor="@color/warning_color" + android:textSize="8sp" + android:textStyle="normal" + app:layout_constrainedWidth="true" + app:layout_constraintBottom_toBottomOf="@id/graph" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/graph" + tools:text="12 345" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/legend_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?android:colorBackground" + android:backgroundTint="@color/color_background_transparent_60" + android:backgroundTintMode="src_in" + android:paddingTop="8dp" + android:paddingBottom="8dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <TextView + android:id="@+id/tag" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:ellipsize="end" + android:importantForAccessibility="no" + android:singleLine="true" + android:textAlignment="textStart" + android:textAppearance="?android:attr/textAppearanceListItem" + android:textColor="?android:textColorPrimary" + android:textStyle="normal" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="#itishashtagtuesdayitishashtagtuesday" /> + + <TextView + android:id="@+id/total_usage" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:importantForAccessibility="no" + android:textAlignment="textEnd" + android:textAppearance="?android:attr/textAppearanceListItemSmall" + android:textColor="?attr/colorPrimary" + android:textStyle="normal|bold" + app:layout_constrainedWidth="false" + app:layout_constraintEnd_toStartOf="@id/barrier2" + app:layout_constraintHorizontal_chainStyle="spread_inside" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/tag" + tools:text="12 345" /> + + <TextView + android:id="@+id/usageLabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="4dp" + android:importantForAccessibility="no" + android:text="@string/total_usage" + android:textAlignment="textEnd" + android:textAppearance="?android:attr/textAppearanceListItemSecondary" + android:textColor="?android:textColorTertiary" + app:layout_constrainedWidth="false" + app:layout_constraintBaseline_toBaselineOf="@+id/total_usage" + app:layout_constraintStart_toEndOf="@id/barrier2" /> + + <TextView + android:id="@+id/total_accounts" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + android:textAlignment="textEnd" + android:textAppearance="?android:attr/textAppearanceListItemSmall" + android:textColor="@color/warning_color" + android:textStyle="normal|bold" + app:layout_constrainedWidth="false" + app:layout_constraintEnd_toStartOf="@id/barrier2" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/total_usage" + tools:text="498" /> + + <TextView + android:id="@+id/accountsLabel" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="4dp" + android:importantForAccessibility="no" + android:text="@string/total_accounts" + android:textAppearance="?android:attr/textAppearanceListItemSecondary" + android:textColor="?android:textColorTertiary" + app:layout_constrainedWidth="true" + app:layout_constraintBaseline_toBaselineOf="@+id/total_accounts" + app:layout_constraintStart_toEndOf="@id/barrier2" /> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/barrier2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="end" + app:constraint_referenced_ids="total_usage,total_accounts" + tools:layout_editor_absoluteY="8dp" /> + </androidx.constraintlayout.widget.ConstraintLayout> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout-sw640dp/fragment_timeline.xml b/app/src/main/res/layout-sw640dp/fragment_timeline.xml new file mode 100644 index 0000000..511b4f3 --- /dev/null +++ b/app/src/main/res/layout-sw640dp/fragment_timeline.xml @@ -0,0 +1,69 @@ +<?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" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?attr/windowBackgroundColor"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="640dp" + android:layout_height="match_parent" + android:layout_gravity="center_horizontal" + android:background="?android:attr/colorBackground"> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <com.keylesspalace.tusky.view.TuskySwipeRefreshLayout + android:id="@+id/swipeRefreshLayout" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerView" + android:clipToPadding="false" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/statusView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginBottom="?attr/actionBarSize" + android:src="@android:color/transparent" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/errorphant_error" + tools:visibility="visible" /> + </FrameLayout> + </com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> + + <androidx.core.widget.ContentLoadingProgressBar + android:id="@+id/topProgressBar" + style="@style/Widget.AppCompat.ProgressBar.Horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:indeterminate="true" + android:visibility="gone" + app:layout_constraintBottom_toTopOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + +</FrameLayout> diff --git a/app/src/main/res/layout-sw640dp/fragment_timeline_notifications.xml b/app/src/main/res/layout-sw640dp/fragment_timeline_notifications.xml new file mode 100644 index 0000000..eef9654 --- /dev/null +++ b/app/src/main/res/layout-sw640dp/fragment_timeline_notifications.xml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright 2023 Tusky Contributors + ~ + ~ This file is a part of Tusky. + ~ + ~ This program is free software; you can redistribute it and/or modify it under the terms of the + ~ GNU General Public License as published by the Free Software Foundation; either version 3 of the + ~ License, or (at your option) any later version. + ~ + ~ Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + ~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + ~ Public License for more details. + ~ + ~ You should have received a copy of the GNU General Public License along with Tusky; if not, + ~ see <http://www.gnu.org/licenses>. + --> + +<FrameLayout 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="?attr/windowBackgroundColor"> + + <androidx.coordinatorlayout.widget.CoordinatorLayout + android:layout_width="640dp" + android:layout_height="match_parent" + android:layout_gravity="center_horizontal" + android:background="?android:attr/colorBackground"> + + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/appBarOptions" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/colorSurface" + app:elevation="0dp"> + + <LinearLayout + android:id="@+id/topButtonsLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <Button + android:id="@+id/buttonClear" + style="@style/TuskyButton.TextButton" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/notifications_clear" + android:textSize="?attr/status_text_medium" /> + + <Button + android:id="@+id/buttonFilter" + style="@style/TuskyButton.TextButton" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/notifications_apply_filter" + android:textSize="?attr/status_text_medium" /> + + </LinearLayout> + + <View + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_gravity="bottom" + android:background="?android:attr/listDivider" + app:layout_scrollFlags="scroll|enterAlways" /> + + </com.google.android.material.appbar.AppBarLayout> + + <com.keylesspalace.tusky.view.TuskySwipeRefreshLayout + android:id="@+id/swipeRefreshLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?android:attr/colorBackground" + android:clipToPadding="false" + android:paddingBottom="@dimen/recyclerview_bottom_padding_actionbutton" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/statusView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:visibility="gone" + tools:visibility="visible" /> + </FrameLayout> + </com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" /> + + </androidx.coordinatorlayout.widget.CoordinatorLayout> +</FrameLayout> diff --git a/app/src/main/res/layout-sw640dp/fragment_view_thread.xml b/app/src/main/res/layout-sw640dp/fragment_view_thread.xml new file mode 100644 index 0000000..3001e4d --- /dev/null +++ b/app/src/main/res/layout-sw640dp/fragment_view_thread.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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"> + + <com.keylesspalace.tusky.view.TuskySwipeRefreshLayout + android:id="@+id/swipeRefreshLayout" + android:layout_width="640dp" + android:layout_height="match_parent" + android:layout_gravity="center_horizontal|top" + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerView" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:background="?android:attr/colorBackground" + android:clipToPadding="false" + android:paddingBottom="@dimen/recyclerview_bottom_padding_no_actionbutton" + android:scrollbarStyle="outsideInset" + android:scrollbars="vertical" /> + + <com.google.android.material.progressindicator.LinearProgressIndicator + android:id="@+id/threadProgressBar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:contentDescription="@string/a11y_label_loading_thread" + android:indeterminate="true" + android:visibility="gone" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/statusView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:visibility="gone" + tools:visibility="visible" /> + </LinearLayout> + </com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> + + <ProgressBar + android:id="@+id/initialProgressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:contentDescription="@string/a11y_label_loading_thread" + android:indeterminate="true" + android:visibility="gone" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml new file mode 100644 index 0000000..0b46cda --- /dev/null +++ b/app/src/main/res/layout/activity_about.xml @@ -0,0 +1,214 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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" + tools:context="com.keylesspalace.tusky.AboutActivity"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + <androidx.core.widget.NestedScrollView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:textDirection="anyRtl"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:focusableInTouchMode="true" + android:gravity="center" + android:orientation="vertical" + android:paddingTop="16dp" + android:paddingBottom="16dp"> + + <ImageView + android:id="@+id/logo" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@mipmap/ic_launcher" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:ignore="ContentDescription" /> + + <TextView + android:id="@+id/versionTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="12dp" + android:textAppearance="@style/TextAppearance.AppCompat.Large" + android:textIsSelectable="true" + android:textStyle="normal" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/logo" + tools:text="Tusky Test" /> + + <TextView + android:id="@+id/deviceInfoTitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginStart="@dimen/text_content_margin" + android:layout_marginEnd="@dimen/text_content_margin" + android:lineSpacingMultiplier="1.1" + android:text="@string/about_device_info_title" + android:textColor="?android:attr/textColorPrimary" + android:textIsSelectable="true" + android:textAppearance="@style/TextAppearance.AppCompat.Medium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/versionTextView" + tools:text="Your device" /> + + <TextView + android:id="@+id/deviceInfo" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:attr/textColorPrimary" + android:textIsSelectable="true" + app:layout_constraintEnd_toEndOf="@+id/deviceInfoTitle" + app:layout_constraintStart_toStartOf="@+id/deviceInfoTitle" + app:layout_constraintTop_toBottomOf="@id/deviceInfoTitle" /> + + <TextView + android:id="@+id/accountInfoTitle" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:lineSpacingMultiplier="1.1" + android:text="@string/about_account_info_title" + android:textAppearance="@style/TextAppearance.AppCompat.Medium" + android:textColor="?android:attr/textColorPrimary" + android:textIsSelectable="true" + android:visibility="gone" + app:layout_constraintEnd_toStartOf="@+id/copyDeviceInfo" + app:layout_constraintStart_toStartOf="@+id/deviceInfo" + app:layout_constraintTop_toBottomOf="@id/deviceInfo" + tools:visibility="visible" /> + + <TextView + android:id="@+id/accountInfo" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:attr/textColorPrimary" + android:textIsSelectable="true" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="@+id/accountInfoTitle" + app:layout_constraintStart_toStartOf="@+id/accountInfoTitle" + app:layout_constraintTop_toBottomOf="@id/accountInfoTitle" + tools:text="\@Tusky@mastodon.social\nVersion: xxx" + tools:visibility="visible" /> + + <ImageButton + android:id="@+id/copyDeviceInfo" + style="@style/TuskyImageButton" + android:layout_width="48dp" + android:layout_height="48dp" + android:contentDescription="@string/about_copy" + android:layout_marginEnd="@dimen/text_content_margin" + app:layout_constraintBottom_toBottomOf="@+id/accountInfo" + app:layout_constraintEnd_toEndOf="parent" + app:srcCompat="@drawable/ic_content_copy_24" /> + + <TextView + android:id="@+id/aboutPoweredByTusky" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="@string/about_powered_by_tusky" + android:textAppearance="@style/TextAppearance.AppCompat.Subhead" + app:layout_constraintEnd_toEndOf="@+id/copyDeviceInfo" + app:layout_constraintStart_toStartOf="@+id/deviceInfo" + app:layout_constraintTop_toBottomOf="@+id/accountInfo" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/aboutLicenseInfoTextView" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:hyphenationFrequency="full" + android:lineSpacingMultiplier="1.2" + android:textIsSelectable="true" + app:layout_constraintEnd_toEndOf="@+id/copyDeviceInfo" + app:layout_constraintStart_toStartOf="@+id/accountInfo" + app:layout_constraintTop_toBottomOf="@id/aboutPoweredByTusky" + tools:text="@string/about_tusky_license" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/aboutWebsiteInfoTextView" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:lineSpacingMultiplier="1.2" + android:textIsSelectable="true" + app:layout_constraintEnd_toEndOf="@+id/aboutLicenseInfoTextView" + app:layout_constraintStart_toStartOf="@+id/aboutLicenseInfoTextView" + app:layout_constraintTop_toBottomOf="@id/aboutLicenseInfoTextView" + tools:text="@string/about_project_site" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/aboutBugsFeaturesInfoTextView" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:lineSpacingMultiplier="1.2" + android:text="@string/about_bug_feature_request_site" + android:textIsSelectable="true" + app:layout_constraintEnd_toEndOf="@+id/aboutWebsiteInfoTextView" + app:layout_constraintStart_toStartOf="@+id/aboutWebsiteInfoTextView" + app:layout_constraintTop_toBottomOf="@id/aboutWebsiteInfoTextView" /> + + <Button + android:id="@+id/tuskyProfileButton" + style="@style/TuskyButton" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="24dp" + android:lineSpacingMultiplier="1.2" + android:maxWidth="320dp" + android:text="@string/about_tusky_account" + android:textAllCaps="false" + android:textSize="16sp" + android:layout_marginEnd="@dimen/text_content_margin" + app:layout_constraintEnd_toStartOf="@+id/aboutLicensesButton" + app:layout_constraintStart_toStartOf="@+id/aboutBugsFeaturesInfoTextView" + app:layout_constraintTop_toBottomOf="@id/aboutBugsFeaturesInfoTextView" /> + + <Button + android:id="@+id/aboutLicensesButton" + style="@style/TuskyButton.Outlined" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:lineSpacingMultiplier="1.2" + android:maxWidth="320dp" + android:text="@string/title_licenses" + android:textAlignment="center" + android:textAllCaps="false" + android:textSize="16sp" + android:layout_marginEnd="@dimen/text_content_margin" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/tuskyProfileButton" + app:layout_constraintTop_toTopOf="@+id/tuskyProfileButton" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + </androidx.core.widget.NestedScrollView> + </FrameLayout> + + <include layout="@layout/item_status_bottom_sheet" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml new file mode 100644 index 0000000..e424a5f --- /dev/null +++ b/app/src/main/res/layout/activity_account.xml @@ -0,0 +1,483 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.keylesspalace.tusky.view.TuskySwipeRefreshLayout 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/swipeToRefreshLayout" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.coordinatorlayout.widget.CoordinatorLayout + android:id="@+id/accountCoordinatorLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fillViewport="true" + android:textDirection="anyRtl"> + + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/accountAppBarLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:elevation="@dimen/actionbar_elevation"> + + <com.google.android.material.appbar.CollapsingToolbarLayout + android:id="@+id/collapsingToolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:contentScrim="?attr/colorSurface" + app:layout_scrollFlags="scroll|exitUntilCollapsed" + app:statusBarScrim="?attr/colorSurface" + app:titleEnabled="false"> + + <ImageView + android:id="@+id/accountHeaderImageView" + android:layout_width="match_parent" + android:layout_height="180dp" + android:layout_alignTop="@+id/account_header_info" + android:background="?attr/colorPrimaryDark" + android:contentDescription="@string/label_header" + android:scaleType="centerCrop" + app:layout_collapseMode="parallax" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:background="#000" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/accountHeaderInfoContainer" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="180dp" + android:paddingStart="16dp" + android:paddingEnd="16dp"> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideAvatar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_begin="@dimen/account_activity_avatar_size" /> + + <Button + android:id="@+id/accountFollowButton" + style="@style/TuskyButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:ellipsize="end" + android:maxLines="1" + android:textSize="?attr/status_text_medium" + app:layout_constrainedWidth="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="1" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toEndOf="@id/accountMuteButton" + app:layout_constraintTop_toTopOf="parent" + tools:text="Follow Requested" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/accountSubscribeButton" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="6dp" + android:minWidth="0dp" + android:paddingStart="8dp" + android:paddingEnd="4dp" + android:scaleType="centerInside" + app:icon="@drawable/ic_notifications_24dp" + app:layout_constrainedHeight="true" + app:layout_constraintBottom_toBottomOf="@+id/accountFollowButton" + app:layout_constraintEnd_toStartOf="@id/accountFollowButton" + app:layout_constraintHorizontal_bias="1" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toStartOf="@id/accountMuteButton" + app:layout_constraintTop_toTopOf="@+id/accountFollowButton" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/accountMuteButton" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="0dp" + android:layout_marginStart="16dp" + android:layout_marginEnd="6dp" + android:minWidth="0dp" + android:paddingStart="8dp" + android:paddingEnd="8dp" + android:scaleType="centerInside" + app:icon="@drawable/ic_unmute_24dp" + app:layout_constrainedHeight="true" + app:layout_constraintBottom_toBottomOf="@+id/accountFollowButton" + app:layout_constraintEnd_toStartOf="@id/accountSubscribeButton" + app:layout_constraintHorizontal_bias="1" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toStartOf="@id/guideAvatar" + app:layout_constraintTop_toTopOf="@+id/accountFollowButton" /> + + <TextView + android:id="@+id/accountDisplayNameTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="62dp" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_large" + android:textStyle="normal|bold" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Tusky Mastodon Client " /> + + <TextView + android:id="@+id/accountUsernameTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/accountDisplayNameTextView" + tools:text="\@Tusky" /> + + <ImageView + android:id="@+id/accountLockedImageView" + android:layout_width="16sp" + android:layout_height="16sp" + android:layout_marginStart="4dp" + android:contentDescription="@string/description_account_locked" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="@+id/accountUsernameTextView" + app:layout_constraintStart_toEndOf="@+id/accountUsernameTextView" + app:layout_constraintTop_toTopOf="@+id/accountUsernameTextView" + app:srcCompat="@drawable/ic_reblog_private_24dp" + app:tint="?android:textColorSecondary" + tools:visibility="visible" /> + + <com.google.android.material.chip.Chip + android:id="@+id/accountFollowsYouTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:clickable="false" + android:focusable="false" + android:minHeight="@dimen/profile_badge_min_height" + android:text="@string/follows_you" + android:textSize="?attr/status_text_small" + android:visibility="gone" + app:chipBackgroundColor="#0000" + app:chipMinHeight="@dimen/profile_badge_min_height" + app:chipStrokeColor="?android:attr/textColorTertiary" + app:chipStrokeWidth="@dimen/profile_badge_stroke_width" + app:ensureMinTouchTargetSize="false" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/accountUsernameTextView" + tools:visibility="visible" /> + + <com.google.android.material.chip.ChipGroup + android:id="@+id/accountBadgeContainer" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + app:chipSpacingVertical="4dp" + app:layout_constraintStart_toEndOf="@id/accountUsernameTextView" + app:layout_constraintTop_toBottomOf="@id/accountFollowsYouTextView" + app:layout_goneMarginStart="0dp" /> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/labelBarrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + app:constraint_referenced_ids="accountFollowsYouTextView,accountBadgeContainer" /> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/accountNoteTextInputLayout" + style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.Dense" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/labelBarrier" + tools:visibility="visible"> + + <com.google.android.material.textfield.TextInputEditText + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/account_note_hint" + android:textDirection="firstStrong" /> + + </com.google.android.material.textfield.TextInputLayout> + + <TextView + android:id="@+id/saveNoteInfo" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/account_note_saved" + android:textColor="?attr/colorPrimary" + android:visibility="gone" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/accountNoteTextInputLayout" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/accountNoteTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:hyphenationFrequency="full" + android:lineSpacingMultiplier="1.1" + android:paddingTop="2dp" + android:textColor="?android:textColorTertiary" + android:textDirection="firstStrong" + android:textIsSelectable="true" + android:textSize="?attr/status_text_medium" + app:layout_constraintTop_toBottomOf="@id/saveNoteInfo" + app:layout_goneMarginTop="8dp" + tools:text="This is a test description. Descriptions can be quite looooong." /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/accountFieldList" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintTop_toBottomOf="@id/accountNoteTextView" + tools:itemCount="2" + tools:listitem="@layout/item_account_field" /> + + <TextView + android:id="@+id/accountDateJoined" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:textColor="@color/textColorSecondary" + app:layout_constraintBottom_toTopOf="@id/accountRemoveView" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/accountFieldList" + tools:text="April, 1971" /> + + <TextView + android:id="@+id/accountRemoveView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:hyphenationFrequency="full" + android:lineSpacingMultiplier="1.1" + android:text="@string/label_remote_account" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/accountDateJoined" + tools:visibility="visible" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/accountMovedView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:visibility="gone" + app:layout_constraintTop_toBottomOf="@id/accountRemoveView" + tools:visibility="visible"> + + <TextView + android:id="@+id/accountMovedText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:drawablePadding="6dp" + android:textSize="?attr/status_text_medium" + app:drawableStartCompat="@drawable/ic_briefcase" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Account has moved" /> + + <ImageView + android:id="@+id/accountMovedAvatar" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_centerVertical="true" + android:layout_marginTop="8dp" + android:layout_marginEnd="24dp" + android:layout_marginBottom="8dp" + android:importantForAccessibility="no" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/accountMovedText" + tools:src="@drawable/avatar_default" /> + + <TextView + android:id="@+id/accountMovedDisplayName" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_large" + android:textStyle="normal|bold" + app:layout_constraintBottom_toTopOf="@id/accountMovedUsername" + app:layout_constraintStart_toEndOf="@id/accountMovedAvatar" + app:layout_constraintTop_toTopOf="@id/accountMovedAvatar" + tools:text="Display name" /> + + <TextView + android:id="@+id/accountMovedUsername" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:layout_constraintBottom_toBottomOf="@id/accountMovedAvatar" + app:layout_constraintStart_toEndOf="@id/accountMovedAvatar" + app:layout_constraintTop_toBottomOf="@id/accountMovedDisplayName" + tools:text="\@username" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <LinearLayout + android:id="@+id/accountStatuses" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackgroundBorderless" + android:gravity="center_horizontal" + android:orientation="vertical" + app:layout_constraintEnd_toStartOf="@id/accountFollowing" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/accountMovedView"> + + <TextView + android:id="@+id/accountStatusesTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:fontFamily="sans-serif-medium" + android:textColor="@color/account_tab_font_color" + android:textSize="?attr/status_text_medium" + tools:text="3000" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingBottom="6dp" + android:text="@string/title_posts" + android:textColor="@color/account_tab_font_color" + android:textSize="?attr/status_text_medium" /> + + </LinearLayout> + + <LinearLayout + android:id="@+id/accountFollowing" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackgroundBorderless" + android:gravity="center_horizontal" + android:orientation="vertical" + app:layout_constraintEnd_toStartOf="@id/accountFollowers" + app:layout_constraintStart_toEndOf="@id/accountStatuses" + app:layout_constraintTop_toBottomOf="@id/accountMovedView"> + + <TextView + android:id="@+id/accountFollowingTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:fontFamily="sans-serif-medium" + android:textColor="@color/account_tab_font_color" + android:textSize="?attr/status_text_medium" + tools:text="500" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingBottom="6dp" + android:text="@string/title_follows" + android:textColor="@color/account_tab_font_color" + android:textSize="?attr/status_text_medium" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/accountFollowers" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackgroundBorderless" + android:gravity="center_horizontal" + android:orientation="vertical" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/accountFollowing" + app:layout_constraintTop_toBottomOf="@id/accountMovedView"> + + <TextView + android:id="@+id/accountFollowersTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:fontFamily="sans-serif-medium" + android:textColor="@color/account_tab_font_color" + android:textSize="?attr/status_text_medium" + tools:text="1234" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingBottom="6dp" + android:text="@string/title_followers" + android:textColor="@color/account_tab_font_color" + android:textSize="?attr/status_text_medium" /> + </LinearLayout> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <!-- top margin equal to statusbar size will be set programmatically --> + <androidx.appcompat.widget.Toolbar + android:id="@+id/accountToolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="top" + android:background="@android:color/transparent" + app:contentInsetStartWithNavigation="0dp" + app:layout_collapseMode="pin" + app:layout_scrollFlags="scroll|enterAlways" /> + + </com.google.android.material.appbar.CollapsingToolbarLayout> + + <com.google.android.material.tabs.TabLayout + android:id="@+id/accountTabLayout" + style="@style/TuskyTabAppearance" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/colorSurface" + app:tabGravity="center" + app:tabMode="scrollable" /> + + </com.google.android.material.appbar.AppBarLayout> + + <androidx.viewpager2.widget.ViewPager2 + android:id="@+id/accountFragmentViewPager" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?android:windowBackground" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/accountFloatingActionButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|end" + android:layout_margin="16dp" + android:contentDescription="@string/action_mention" + app:srcCompat="@drawable/ic_create_24dp" + app:tint="?attr/colorOnPrimary" /> + + <include layout="@layout/item_status_bottom_sheet" /> + + <ImageView + android:id="@+id/accountAvatarImageView" + android:layout_width="@dimen/account_activity_avatar_size" + android:layout_height="@dimen/account_activity_avatar_size" + android:layout_marginStart="16dp" + android:contentDescription="@string/label_avatar" + android:padding="3dp" + app:layout_anchor="@+id/accountHeaderInfoContainer" + app:layout_anchorGravity="top" + app:layout_scrollFlags="scroll" + app:srcCompat="@drawable/avatar_default" /> + + </androidx.coordinatorlayout.widget.CoordinatorLayout> + +</com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> diff --git a/app/src/main/res/layout/activity_account_list.xml b/app/src/main/res/layout/activity_account_list.xml new file mode 100644 index 0000000..8023e1d --- /dev/null +++ b/app/src/main/res/layout/activity_account_list.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context="com.keylesspalace.tusky.components.accountlist.AccountListActivity"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <androidx.fragment.app.FragmentContainerView + android:id="@+id/fragment_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + <include layout="@layout/item_status_bottom_sheet" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/activity_announcements.xml b/app/src/main/res/layout/activity_announcements.xml new file mode 100644 index 0000000..6a18bf1 --- /dev/null +++ b/app/src/main/res/layout/activity_announcements.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" /> + + <com.keylesspalace.tusky.view.TuskySwipeRefreshLayout + android:id="@+id/swipeRefreshLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/announcementsList" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="@dimen/recyclerview_bottom_padding_no_actionbutton" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/errorMessageView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:src="@android:color/transparent" + android:visibility="gone" + tools:src="@drawable/errorphant_error" + tools:visibility="visible" /> + </FrameLayout> + </com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> + + <include layout="@layout/item_status_bottom_sheet" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml new file mode 100644 index 0000000..7e3bae8 --- /dev/null +++ b/app/src/main/res/layout/activity_compose.xml @@ -0,0 +1,386 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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/activityCompose" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="8dp" + android:background="@android:color/transparent"> + + <ImageView + android:id="@+id/composeAvatar" + android:layout_width="?attr/actionBarSize" + android:layout_height="?attr/actionBarSize" + android:layout_gravity="end" + android:padding="8dp" + tools:ignore="ContentDescription" /> + <!--content description will be set in code --> + + <Spinner + android:id="@+id/composePostLanguageButton" + style="@style/TuskyImageButton" + android:layout_width="52dp" + android:layout_height="wrap_content" + android:layout_gravity="end" + android:padding="0dp" + android:contentDescription="@string/description_post_language" + android:textColor="?android:attr/textColorTertiary" + android:textSize="?attr/status_text_large" + android:textStyle="bold" + app:tooltipText="@string/description_post_language" /> + + <androidx.appcompat.widget.AppCompatButton + android:id="@+id/atButton" + style="@style/TuskyImageButton" + android:layout_width="40dp" + android:layout_height="wrap_content" + android:layout_gravity="end" + android:padding="8dp" + android:text="@string/at_symbol" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_large" + android:textStyle="bold" /> + + <androidx.appcompat.widget.AppCompatButton + android:id="@+id/hashButton" + style="@style/TuskyImageButton" + android:layout_width="40dp" + android:layout_height="wrap_content" + android:layout_gravity="end" + android:padding="8dp" + android:text="@string/hash_symbol" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_large" + android:textStyle="bold" /> + + <ImageButton + android:id="@+id/descriptionMissingWarningButton" + style="@style/TuskyImageButton" + android:layout_width="40dp" + android:layout_height="wrap_content" + android:layout_gravity="end" + android:contentDescription="@string/hint_media_description_missing" + android:padding="8dp" + app:srcCompat="@drawable/ic_missing_description_24dp" + app:tint="@color/tusky_orange_light" + app:tooltipText="@string/hint_media_description_missing" + android:visibility="invisible" /> + </androidx.appcompat.widget.Toolbar> + + <androidx.core.widget.NestedScrollView + android:id="@+id/composeMainScrollView" + android:layout_width="match_parent" + android:layout_height="@dimen/compose_activity_scrollview_height" + android:layout_marginTop="?attr/actionBarSize" + android:layout_marginBottom="52dp"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <TextView + android:id="@+id/composeUsernameView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" + android:layout_marginBottom="0dp" + android:textSize="?attr/status_text_small" + android:textStyle="bold" + android:visibility="gone" + tools:text="Posting as @username@domain" + tools:visibility="visible" /> + + <TextView + android:id="@+id/composeReplyView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginLeft="16dp" + android:layout_marginRight="16dp" + android:layout_marginBottom="6dp" + android:drawablePadding="6dp" + android:textSize="?attr/status_text_small" + android:textStyle="bold" + android:visibility="gone" + tools:text="Reply to @username" + tools:visibility="visible" /> + + <TextView + android:id="@+id/composeReplyContentView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="2dp" + android:background="?attr/colorBackgroundAccent" + android:lineSpacingMultiplier="1.1" + android:paddingLeft="16dp" + android:paddingTop="4dp" + android:paddingRight="16dp" + android:paddingBottom="4dp" + android:textSize="?attr/status_text_small" + android:visibility="gone" + tools:text="Post content which may be preeettyy long, so please, make sure there's enough room for everything, okay? Not kidding. I wish Eugen answered me more often, sigh." + tools:visibility="visible" /> + + <LinearLayout + android:id="@+id/composeContentWarningBar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <androidx.emoji2.widget.EmojiEditText + android:id="@+id/composeContentWarningField" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:background="@android:color/transparent" + android:hint="@string/hint_content_warning" + android:inputType="text|textCapSentences" + android:lineSpacingMultiplier="1.1" + android:maxLines="1" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:textColorHint="?android:attr/textColorTertiary" + android:textSize="?attr/status_text_medium" /> + + <View + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_marginTop="8dp" + android:background="?android:attr/listDivider" /> + + </LinearLayout> + + <com.keylesspalace.tusky.components.compose.view.EditTextTyped + android:id="@+id/composeEditField" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@null" + android:completionThreshold="2" + android:dropDownWidth="wrap_content" + android:hint="@string/hint_compose" + android:inputType="text|textMultiLine|textCapSentences" + android:lineSpacingMultiplier="1.1" + android:paddingLeft="16dp" + android:paddingTop="8dp" + android:paddingRight="16dp" + android:paddingBottom="8dp" + android:textColorHint="?android:attr/textColorTertiary" + android:textSize="?attr/status_text_large" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/composeMediaPreviewBar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:scrollbars="none" + android:visibility="gone" /> + + <com.keylesspalace.tusky.components.compose.view.PollPreviewView + android:id="@+id/pollPreview" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="@dimen/poll_preview_min_width" + android:visibility="gone" + tools:visibility="visible" /> + + </LinearLayout> + + </androidx.core.widget.NestedScrollView> + + <LinearLayout + android:id="@+id/addMediaBottomSheet" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/colorSurface" + android:elevation="12dp" + android:orientation="vertical" + android:paddingStart="16dp" + android:paddingTop="8dp" + android:paddingEnd="16dp" + android:paddingBottom="52dp" + app:behavior_hideable="true" + app:behavior_peekHeight="0dp" + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"> + + <TextView + android:id="@+id/actionPhotoTake" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:drawablePadding="8dp" + android:padding="8dp" + android:text="@string/action_photo_take" + android:textSize="?attr/status_text_medium" /> + + <TextView + android:id="@+id/actionPhotoPick" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:drawablePadding="8dp" + android:padding="8dp" + android:text="@string/action_add_media" + android:textSize="?attr/status_text_medium" /> + + <TextView + android:id="@+id/addPollTextActionTextView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:drawablePadding="8dp" + android:padding="8dp" + android:text="@string/action_add_poll" + android:textSize="?attr/status_text_medium" /> + </LinearLayout> + + <com.keylesspalace.tusky.view.EmojiPicker + android:id="@+id/emojiView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/colorSurface" + android:elevation="12dp" + android:paddingStart="16dp" + android:paddingTop="8dp" + android:paddingEnd="16dp" + android:paddingBottom="60dp" + app:behavior_hideable="true" + app:behavior_peekHeight="0dp" + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" /> + + <com.keylesspalace.tusky.components.compose.view.ComposeOptionsView + android:id="@+id/composeOptionsBottomSheet" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/colorSurface" + android:elevation="12dp" + android:paddingStart="24dp" + android:paddingTop="12dp" + android:paddingEnd="24dp" + android:paddingBottom="60dp" + app:behavior_hideable="true" + app:behavior_peekHeight="0dp" + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" /> + + <com.keylesspalace.tusky.components.compose.view.ComposeScheduleView + android:id="@+id/composeScheduleView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/colorSurface" + android:elevation="12dp" + android:paddingStart="16dp" + android:paddingTop="8dp" + android:paddingEnd="16dp" + android:paddingBottom="52dp" + app:behavior_hideable="true" + app:behavior_peekHeight="0dp" + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior" /> + + <LinearLayout + android:id="@+id/composeBottomBar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:animateLayoutChanges="true" + android:background="?attr/colorSurface" + android:elevation="12dp" + android:gravity="center_vertical" + android:paddingStart="8dp" + android:paddingTop="4dp" + android:paddingEnd="8dp" + android:paddingBottom="4dp"> + + <ImageButton + android:id="@+id/composeAddMediaButton" + style="@style/TuskyImageButton" + android:layout_width="36dp" + android:layout_height="36dp" + android:layout_marginEnd="4dp" + android:contentDescription="@string/action_add_media" + android:padding="4dp" + app:srcCompat="@drawable/ic_attach_file_24dp" + app:tooltipText="@string/action_add_media" /> + + <ImageButton + android:id="@+id/composeToggleVisibilityButton" + style="@style/TuskyImageButton" + android:layout_width="36dp" + android:layout_height="36dp" + android:layout_marginEnd="4dp" + android:contentDescription="@string/action_toggle_visibility" + android:padding="4dp" + app:tint="?android:attr/textColorTertiary" + app:tooltipText="@string/action_toggle_visibility" + tools:src="@drawable/ic_public_24dp" /> + + <ImageButton + android:id="@+id/composeHideMediaButton" + style="@style/TuskyImageButton" + android:layout_width="36dp" + android:layout_height="36dp" + android:layout_marginEnd="4dp" + android:contentDescription="@string/action_hide_media" + android:padding="4dp" + app:tooltipText="@string/action_hide_media" + tools:src="@drawable/ic_eye_24dp" /> + + <ImageButton + android:id="@+id/composeContentWarningButton" + style="@style/TuskyImageButton" + android:layout_width="36dp" + android:layout_height="36dp" + android:layout_marginEnd="4dp" + android:contentDescription="@string/action_content_warning" + android:padding="4dp" + app:srcCompat="@drawable/ic_cw_24dp" + app:tooltipText="@string/action_content_warning" /> + + <ImageButton + android:id="@+id/composeEmojiButton" + style="@style/TuskyImageButton" + android:layout_width="36dp" + android:layout_height="36dp" + android:layout_marginEnd="4dp" + android:contentDescription="@string/action_emoji_keyboard" + android:padding="4dp" + app:srcCompat="@drawable/ic_emoji_24dp" + app:tooltipText="@string/action_emoji_keyboard" /> + + <ImageButton + android:id="@+id/composeScheduleButton" + style="@style/TuskyImageButton" + android:layout_width="36dp" + android:layout_height="36dp" + android:layout_marginEnd="4dp" + android:contentDescription="@string/action_schedule_post" + android:padding="4dp" + app:srcCompat="@drawable/ic_access_time" + app:tooltipText="@string/action_schedule_post" /> + + <Space + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" /> + + <TextView + android:id="@+id/composeCharactersLeftView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" + android:textStyle="bold" + tools:text="500" /> + + <com.keylesspalace.tusky.components.compose.view.TootButton + android:id="@+id/composeTootButton" + style="@style/TuskyButton" + android:layout_width="@dimen/toot_button_width" + android:layout_height="wrap_content" + android:layout_marginStart="10dp" + android:textSize="?attr/status_text_medium" /> + + </LinearLayout> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/activity_drafts.xml b/app/src/main/res/layout/activity_drafts.xml new file mode 100644 index 0000000..41cf705 --- /dev/null +++ b/app/src/main/res/layout/activity_drafts.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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" + tools:context=".components.drafts.DraftsActivity"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/draftsRecyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="@dimen/recyclerview_bottom_padding_no_actionbutton" + android:scrollbarStyle="outsideInset" + android:scrollbars="vertical" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/draftsErrorMessageView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerInParent="true" + android:layout_gravity="center" + android:src="@android:color/transparent" + android:visibility="gone" + tools:src="@drawable/errorphant_error" + tools:visibility="visible" /> + + <include + android:id="@+id/bottomSheet" + layout="@layout/item_status_bottom_sheet" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/activity_edit_filter.xml b/app/src/main/res/layout/activity_edit_filter.xml new file mode 100644 index 0000000..2a56297 --- /dev/null +++ b/app/src/main/res/layout/activity_edit_filter.xml @@ -0,0 +1,177 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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" + tools:context="com.keylesspalace.tusky.components.filters.EditFilterActivity"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <androidx.core.widget.NestedScrollView + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/filter_title_wrapper" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:hint="@string/label_filter_title"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/filterTitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textNoSuggestions" /> + </com.google.android.material.textfield.TextInputLayout> + + <TextView + style="@style/TextAppearance.Material3.TitleSmall" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="@string/label_filter_keywords" + android:textColor="?attr/colorAccent" /> + + <com.google.android.material.chip.ChipGroup + android:id="@+id/keywordChips" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <com.google.android.material.chip.Chip + android:id="@+id/actionChip" + style="@style/Widget.MaterialComponents.Chip.Action" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:checkable="false" + android:text="@string/action_add" + android:textColor="?attr/colorOnPrimary" + app:chipIcon="@drawable/ic_plus_24dp" + app:chipIconTint="?attr/colorOnPrimary" + app:chipSurfaceColor="?attr/colorPrimary" /> + </com.google.android.material.chip.ChipGroup> + + <TextView + style="@style/TextAppearance.Material3.TitleSmall" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="@string/label_filter_action" + android:textColor="?attr/colorAccent" /> + + <RadioGroup + android:id="@+id/filter_action_group" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <RadioButton + android:id="@+id/filter_action_warn" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="48dp" + android:text="@string/filter_description_warn" /> + + <RadioButton + android:id="@+id/filter_action_hide" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="48dp" + android:text="@string/filter_description_hide" /> + </RadioGroup> + + <TextView + style="@style/TextAppearance.Material3.TitleSmall" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="@string/label_duration" + android:textColor="?attr/colorAccent" /> + + <Spinner + android:id="@+id/filterDurationSpinner" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:entries="@array/filter_duration_names" + android:minHeight="48dp" /> + + <TextView + style="@style/TextAppearance.Material3.TitleSmall" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="@string/label_filter_context" + android:textColor="?attr/colorAccent" /> + + <com.google.android.material.switchmaterial.SwitchMaterial + android:id="@+id/filter_context_home" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="48dp" + android:text="@string/title_home" /> + + <com.google.android.material.switchmaterial.SwitchMaterial + android:id="@+id/filter_context_notifications" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="48dp" + android:text="@string/title_notifications" /> + + <com.google.android.material.switchmaterial.SwitchMaterial + android:id="@+id/filter_context_public" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="48dp" + android:text="@string/pref_title_public_filter_keywords" /> + + <com.google.android.material.switchmaterial.SwitchMaterial + android:id="@+id/filter_context_thread" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="48dp" + android:text="@string/pref_title_thread_filter_keywords" /> + + <com.google.android.material.switchmaterial.SwitchMaterial + android:id="@+id/filter_context_account" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="48dp" + android:text="@string/pref_title_account_filter_keywords" /> + + <LinearLayout + style="?android:attr/buttonBarStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="end" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <Button + android:id="@+id/filter_delete_button" + style="?android:attr/buttonBarButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/action_delete" /> + + <Button + android:id="@+id/filter_save_button" + style="?android:attr/buttonBarButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:text="@string/action_save" /> + </LinearLayout> + + </LinearLayout> + </androidx.core.widget.NestedScrollView> +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/activity_edit_profile.xml b/app/src/main/res/layout/activity_edit_profile.xml new file mode 100644 index 0000000..60fce28 --- /dev/null +++ b/app/src/main/res/layout/activity_edit_profile.xml @@ -0,0 +1,174 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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" + tools:context=".EditProfileActivity"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <androidx.core.widget.NestedScrollView + android:id="@+id/scrollView" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:focusableInTouchMode="true"> + + <ImageView + android:id="@+id/headerPreview" + android:layout_width="match_parent" + android:layout_height="200dp" + android:contentDescription="@null" + app:layout_constraintTop_toTopOf="parent" /> + + <ImageButton + android:id="@+id/headerButton" + android:layout_width="match_parent" + android:layout_height="200dp" + android:background="#66000000" + android:contentDescription="@string/label_header" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/ic_add_a_photo_32dp" /> + + <ImageView + android:id="@+id/avatarPreview" + android:layout_width="80dp" + android:layout_height="80dp" + android:layout_marginStart="16dp" + android:contentDescription="@null" + app:layout_constraintBottom_toBottomOf="@id/headerPreview" + app:layout_constraintStart_toStartOf="@id/contentContainer" + app:layout_constraintTop_toBottomOf="@id/headerPreview" /> + + <ImageButton + android:id="@+id/avatarButton" + android:layout_width="80dp" + android:layout_height="80dp" + android:layout_marginStart="16dp" + android:background="@drawable/round_button" + android:contentDescription="@string/label_avatar" + android:elevation="4dp" + app:layout_constraintBottom_toBottomOf="@id/headerPreview" + app:layout_constraintStart_toStartOf="@id/contentContainer" + app:layout_constraintTop_toBottomOf="@id/headerPreview" + app:srcCompat="@drawable/ic_add_a_photo_32dp" /> + + <LinearLayout + android:id="@+id/contentContainer" + android:layout_width="@dimen/timeline_width" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:orientation="vertical" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/avatarPreview"> + + <com.google.android.material.textfield.TextInputLayout + style="@style/TuskyTextInput" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="16dp" + android:hint="@string/hint_display_name"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/displayNameEditText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:importantForAutofill="no" + android:lines="1" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + style="@style/TuskyTextInput" + android:id="@+id/noteEditTextLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="16dp" + android:layout_marginEnd="16dp" + android:hint="@string/hint_note" + app:counterEnabled="true" + app:counterTextColor="?android:textColorTertiary"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/noteEditText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:importantForAutofill="no" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.checkbox.MaterialCheckBox + android:id="@+id/lockedCheckBox" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="30dp" + android:layout_marginEnd="16dp" + android:paddingStart="8dp" + android:text="@string/lock_account_label" + android:textSize="?attr/status_text_medium" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginBottom="24dp" + android:paddingStart="40dp" + android:text="@string/lock_account_label_description" + android:textSize="?attr/status_text_small" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginBottom="8dp" + android:text="@string/profile_metadata_label" + android:textSize="?attr/status_text_small" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/fieldList" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:nestedScrollingEnabled="false" /> + + <Button + android:id="@+id/addFieldButton" + style="@style/TuskyButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginBottom="16dp" + android:drawablePadding="6dp" + android:text="@string/profile_metadata_add" + android:textColor="#fff" /> + + </LinearLayout> + </androidx.constraintlayout.widget.ConstraintLayout> + + </androidx.core.widget.NestedScrollView> + + <ProgressBar + android:id="@+id/saveProgressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:indeterminate="true" + android:visibility="gone" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/activity_filters.xml b/app/src/main/res/layout/activity_filters.xml new file mode 100644 index 0000000..f170730 --- /dev/null +++ b/app/src/main/res/layout/activity_filters.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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" + tools:context="com.keylesspalace.tusky.components.filters.FiltersActivity"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <com.keylesspalace.tusky.view.TuskySwipeRefreshLayout + android:id="@+id/swipeRefreshLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginTop="?attr/actionBarSize"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/filtersList" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="@dimen/recyclerview_bottom_padding_actionbutton" + android:scrollbarStyle="outsideInset" + android:scrollbars="vertical" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/messageView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" /> + </FrameLayout> + </com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" /> + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/addFilterButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:contentDescription="@string/filter_addition_title" + app:layout_anchor="@id/filtersList" + app:layout_anchorGravity="bottom|end" + app:srcCompat="@drawable/ic_plus_24dp" + app:tint="?attr/colorOnPrimary" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/activity_followed_tags.xml b/app/src/main/res/layout/activity_followed_tags.xml new file mode 100644 index 0000000..cc6478a --- /dev/null +++ b/app/src/main/res/layout/activity_followed_tags.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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" + tools:context="com.keylesspalace.tusky.components.followedtags.FollowedTagsActivity"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/followedTagsView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="@dimen/recyclerview_bottom_padding_actionbutton" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + tools:itemCount="5" + tools:listitem="@layout/item_followed_hashtag" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/followedTagsMessageView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + tools:visibility="gone" /> + + <ProgressBar + android:id="@+id/followedTagsProgressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + tools:visibility="gone" /> + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/fab" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|end" + android:layout_margin="16dp" + android:contentDescription="@string/action_follow_hashtag" + app:srcCompat="@drawable/ic_hashtag" + app:tint="?attr/colorOnPrimary" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/activity_license.xml b/app/src/main/res/layout/activity_license.xml new file mode 100644 index 0000000..38e2667 --- /dev/null +++ b/app/src/main/res/layout/activity_license.xml @@ -0,0 +1,232 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:license="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".LicenseActivity"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <ScrollView + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:orientation="vertical" + android:textDirection="anyRtl" + android:paddingBottom="12dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="18dp" + android:layout_marginStart="18dp" + android:layout_marginTop="12dp" + android:gravity="center_vertical" + android:lineSpacingMultiplier="1.1" + android:text="@string/license_description" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_apache_2" + license:link="https://kotlinlang.org/" + license:name="Kotlin" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_apache_2" + license:link="https://developer.android.com/jetpack/androidx" + license:name="AndroidX" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_apache_2" + license:link="https://github.com/material-components/material-components-android" + license:name="Material Components for Android" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_apache_2" + license:link="https://square.github.io/okhttp/" + license:name="OkHttp" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_apache_2" + license:link="https://github.com/google/conscrypt" + license:name="Conscrypt" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_apache_2" + license:link="https://square.github.io/retrofit/" + license:name="Retrofit" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_apache_2" + license:link="https://github.com/square/moshi" + license:name="Moshi" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_apache_2" + license:link="https://bumptech.github.io/glide/" + license:name="Glide" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_apache_2" + license:link="https://dagger.dev/" + license:name="Dagger 2" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_apache_2" + license:link="https://github.com/mikepenz/MaterialDrawer" + license:name="MaterialDrawer" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_apache_2" + license:link="https://github.com/connyduck/SparkButton" + license:name="SparkButton" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_apache_2" + license:link="https://github.com/chrisbanes/PhotoView" + license:name="PhotoView" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_apache_2" + license:link="https://github.com/CanHub/Android-Image-Cropper" + license:name="Android Image Cropper" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_apache_2" + license:link="https://github.com/penfeizhou/APNG4Android" + license:name="APNG4Android" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_apache_2" + license:link="https://github.com/C1710/FileMojiCompat" + license:name="FileMojiCompat" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_cc_by_4" + license:link="https://twemoji.twitter.com/" + license:name="Twemoji" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_cc_by_4" + license:link="https://github.com/c1710/blobmoji" + license:name="Blobmoji" /> + + <com.keylesspalace.tusky.view.LicenseCard + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="12dp" + android:layout_marginStart="12dp" + android:layout_marginTop="12dp" + license:license="@string/license_cc_by_sa_4" + license:link="https://github.com/tuskyapp/artwork" + license:name="Tusky elephant artwork" /> + + <TextView + android:id="@+id/licenseApacheTextView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginEnd="18dp" + android:layout_marginStart="18dp" + android:layout_marginTop="12dp" + android:textSize="12sp" /> + + </LinearLayout> + + </ScrollView> + + <include layout="@layout/item_status_bottom_sheet" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/activity_lists.xml b/app/src/main/res/layout/activity_lists.xml new file mode 100644 index 0000000..302f747 --- /dev/null +++ b/app/src/main/res/layout/activity_lists.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <com.keylesspalace.tusky.view.TuskySwipeRefreshLayout + android:id="@+id/swipeRefreshLayout" + android:layout_width="0dp" + android:layout_height="0dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@id/includedToolbar"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/listsRecycler" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="@dimen/recyclerview_bottom_padding_actionbutton" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/messageView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone" + tools:visibility="visible" /> + </FrameLayout> + </com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/addListButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:contentDescription="@string/action_create_list" + app:layout_anchor="@id/listsRecycler" + app:layout_anchorGravity="bottom|end" + app:srcCompat="@drawable/ic_plus_24dp" + app:tint="?attr/colorOnPrimary" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..c8ce507 --- /dev/null +++ b/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,105 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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:gravity="center" + android:orientation="vertical" + tools:context="com.keylesspalace.tusky.components.login.LoginActivity"> + + <ScrollView + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fillViewport="true" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="vertical" + android:padding="16dp"> + + <ImageView + android:layout_width="160dp" + android:layout_height="178dp" + android:layout_marginBottom="50dp" + android:contentDescription="@null" + android:id="@+id/loginLogo" + app:srcCompat="@drawable/elephant_friend" /> + + <LinearLayout + android:id="@+id/loginInputLayout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:clipChildren="false" + android:orientation="vertical"> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/domainTextInputLayout" + style="@style/TuskyTextInput" + android:layout_width="250dp" + android:layout_height="wrap_content" + android:hint="@string/hint_domain" + app:errorEnabled="true"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/domainEditText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ems="10" + android:inputType="textUri" /> + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.button.MaterialButton + android:id="@+id/loginButton" + style="@style/TuskyButton" + android:layout_width="250dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:gravity="center" + android:text="@string/action_login" /> + + <TextView + android:id="@+id/whatsAnInstanceTextView" + android:layout_width="250dp" + android:layout_height="wrap_content" + android:paddingTop="8dp" + android:text="@string/link_whats_an_instance" + android:textAlignment="center" /> + </LinearLayout> + + <LinearLayout + android:id="@+id/loginLoadingLayout" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + android:visibility="gone"> + + <ProgressBar + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" /> + + <TextView + android:layout_width="250dp" + android:layout_height="wrap_content" + android:paddingTop="10dp" + android:text="@string/login_connection" + android:textAlignment="center" /> + </LinearLayout> + + </LinearLayout> + </ScrollView> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:layout_alignParentTop="true" + android:background="@android:color/transparent" + app:layout_constraintTop_toTopOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_login_webview.xml b/app/src/main/res/layout/activity_login_webview.xml new file mode 100644 index 0000000..10a6e30 --- /dev/null +++ b/app/src/main/res/layout/activity_login_webview.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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"> + + <com.google.android.material.appbar.AppBarLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/loginToolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + </com.google.android.material.appbar.AppBarLayout> + + <ProgressBar + android:id="@+id/loginProgress" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> + + <WebView + android:id="@+id/loginWebView" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" /> + + <TextView + android:id="@+id/loginRules" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:attr/colorBackground" + android:drawablePadding="8dp" + android:foreground="?attr/selectableItemBackgroundBorderless" + android:lineSpacingMultiplier="1.1" + android:paddingHorizontal="24dp" + android:paddingVertical="12dp" + app:drawableStartCompat="@drawable/info_24dp" + tools:text="By logging in you agree to the rules of instance.example" /> + + </LinearLayout> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..7689a3d --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,111 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.drawerlayout.widget.DrawerLayout 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/mainDrawerLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:fitsSystemWindows="true"> + + <androidx.coordinatorlayout.widget.CoordinatorLayout + android:id="@+id/mainCoordinatorLayout" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context="com.keylesspalace.tusky.MainActivity"> + + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/appBar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:elevation="@dimen/actionbar_elevation" + app:elevationOverlayEnabled="false"> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/mainToolbar" + style="@style/Widget.AppCompat.Toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:contentInsetStartWithNavigation="0dp" + app:layout_scrollFlags="scroll|enterAlways" + app:navigationContentDescription="@string/action_open_drawer" /> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/topNav" + android:layout_width="match_parent" + android:layout_height="48dp" + android:orientation="horizontal" + app:contentInsetStart="0dp" + app:contentInsetStartWithNavigation="0dp" + app:navigationContentDescription="@string/action_open_drawer"> + + <com.keylesspalace.tusky.view.AdaptiveTabLayout + android:id="@+id/tabLayout" + style="@style/TuskyTabAppearance" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:tabGravity="fill" + app:tabMaxWidth="0dp" + app:tabMode="scrollable" /> + </androidx.appcompat.widget.Toolbar> + + </com.google.android.material.appbar.AppBarLayout> + + <androidx.viewpager2.widget.ViewPager2 + android:id="@+id/viewPager" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginBottom="?attr/actionBarSize" + android:background="?attr/windowBackgroundColor" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + <com.google.android.material.bottomappbar.BottomAppBar + android:id="@+id/bottomNav" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + app:contentInsetStart="0dp" + app:contentInsetStartWithNavigation="0dp" + app:fabAlignmentMode="end" + app:navigationContentDescription="@string/action_open_drawer"> + + <com.keylesspalace.tusky.view.AdaptiveTabLayout + android:id="@+id/bottomTabLayout" + style="@style/TuskyTabAppearance" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + app:tabGravity="fill" + app:tabIndicatorGravity="top" + app:tabMode="scrollable" /> + + </com.google.android.material.bottomappbar.BottomAppBar> + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/composeButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/fabMargin" + android:contentDescription="@string/action_compose" + app:layout_anchor="@id/viewPager" + app:layout_anchorGravity="bottom|end" + app:tint="?attr/colorOnPrimary" + app:srcCompat="@drawable/ic_create_24dp" /> + + <include layout="@layout/item_status_bottom_sheet" /> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:visibility="gone" /> + + </androidx.coordinatorlayout.widget.CoordinatorLayout> + + <com.mikepenz.materialdrawer.widget.MaterialDrawerSliderView + android:id="@+id/mainDrawer" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_gravity="start" + android:fitsSystemWindows="true" /> + +</androidx.drawerlayout.widget.DrawerLayout> diff --git a/app/src/main/res/layout/activity_preferences.xml b/app/src/main/res/layout/activity_preferences.xml new file mode 100644 index 0000000..ef4ea7a --- /dev/null +++ b/app/src/main/res/layout/activity_preferences.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context="com.keylesspalace.tusky.components.preference.PreferencesActivity"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <androidx.fragment.app.FragmentContainerView + android:id="@+id/fragment_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_report.xml b/app/src/main/res/layout/activity_report.xml new file mode 100644 index 0000000..32d1f02 --- /dev/null +++ b/app/src/main/res/layout/activity_report.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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" + tools:context=".components.report.ReportActivity"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <androidx.viewpager2.widget.ViewPager2 + android:id="@+id/wizard" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:overScrollMode="never" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + <include layout="@layout/item_status_bottom_sheet" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_scheduled_status.xml b/app/src/main/res/layout/activity_scheduled_status.xml new file mode 100644 index 0000000..576af54 --- /dev/null +++ b/app/src/main/res/layout/activity_scheduled_status.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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" + tools:context=".components.scheduled.ScheduledStatusActivity"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <com.keylesspalace.tusky.view.TuskySwipeRefreshLayout + android:id="@+id/swipeRefreshLayout" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/scheduledTootList" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/errorMessageView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:visibility="gone" + tools:src="@drawable/errorphant_error" + tools:visibility="visible" /> + </FrameLayout> + </com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> + </androidx.constraintlayout.widget.ConstraintLayout> +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml new file mode 100644 index 0000000..993cad3 --- /dev/null +++ b/app/src/main/res/layout/activity_search.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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" + tools:context="com.keylesspalace.tusky.components.search.SearchActivity"> + + <com.google.android.material.appbar.AppBarLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:elevation="@dimen/actionbar_elevation" + app:layout_collapseMode="pin"> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:elevation="@dimen/actionbar_elevation" + app:contentInsetStartWithNavigation="0dp" + app:layout_scrollFlags="scroll|snap|enterAlways" + app:navigationIcon="?attr/homeAsUpIndicator" /> + + <com.google.android.material.tabs.TabLayout + android:id="@+id/tabs" + style="@style/TuskyTabAppearance" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:tabGravity="fill" + app:tabMaxWidth="0dp" + app:tabMode="fixed" /> + + </com.google.android.material.appbar.AppBarLayout> + + <androidx.viewpager2.widget.ViewPager2 + android:id="@+id/pages" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + <include layout="@layout/item_status_bottom_sheet" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_statuslist.xml b/app/src/main/res/layout/activity_statuslist.xml new file mode 100644 index 0000000..883253a --- /dev/null +++ b/app/src/main/res/layout/activity_statuslist.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context="com.keylesspalace.tusky.StatusListActivity"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <androidx.fragment.app.FragmentContainerView + android:id="@+id/fragmentContainer" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + <include layout="@layout/item_status_bottom_sheet" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_tab_preference.xml b/app/src/main/res/layout/activity_tab_preference.xml new file mode 100644 index 0000000..e5e35c4 --- /dev/null +++ b/app/src/main/res/layout/activity_tab_preference.xml @@ -0,0 +1,76 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/currentTabsRecyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + app:layout_constraintTop_toBottomOf="@id/appbar" /> + + <View + android:id="@+id/scrim" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?attr/scrimBackground" + android:visibility="invisible" /> + + <com.google.android.material.floatingactionbutton.FloatingActionButton + android:id="@+id/actionButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|end" + android:layout_margin="16dp" + android:contentDescription="@string/action_add_tab" + app:srcCompat="@drawable/ic_plus_24dp" + app:tint="?attr/colorOnPrimary" /> + + <com.google.android.material.card.MaterialCardView + android:id="@+id/sheet" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|end" + android:layout_margin="16dp" + android:visibility="invisible" + app:cardBackgroundColor="?attr/colorSurface" + app:cardElevation="2dp"> + + <LinearLayout + android:layout_width="240dp" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/addTabRecyclerView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:overScrollMode="never" /> + + <TextView + android:layout_width="match_parent" + android:layout_height="48dp" + android:layout_gravity="bottom" + android:background="?attr/colorPrimary" + android:drawablePadding="12dp" + android:ellipsize="end" + android:gravity="center_vertical" + android:lines="1" + android:paddingStart="8dp" + android:paddingEnd="8dp" + android:text="@string/action_add_tab" + android:textColor="?attr/colorOnPrimary" + android:textSize="?attr/status_text_large" + app:drawableStartCompat="@drawable/ic_plus_24dp" + app:drawableTint="?attr/colorOnPrimary" /> + </LinearLayout> + + </com.google.android.material.card.MaterialCardView> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/activity_trending.xml b/app/src/main/res/layout/activity_trending.xml new file mode 100644 index 0000000..d51fdf5 --- /dev/null +++ b/app/src/main/res/layout/activity_trending.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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" + tools:context="com.keylesspalace.tusky.components.trending.TrendingActivity"> + + <include + android:id="@+id/includedToolbar" + layout="@layout/toolbar_basic" /> + + <androidx.fragment.app.FragmentContainerView + android:id="@+id/fragmentContainer" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + <include layout="@layout/item_status_bottom_sheet" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_view_media.xml b/app/src/main/res/layout/activity_view_media.xml new file mode 100644 index 0000000..330616f --- /dev/null +++ b/app/src/main/res/layout/activity_view_media.xml @@ -0,0 +1,30 @@ +<?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="match_parent" + android:layout_height="match_parent" + android:background="@color/black" + android:orientation="vertical"> + + <androidx.viewpager2.widget.ViewPager2 + android:id="@+id/viewPager" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:background="@color/transparent_black" + android:theme="@style/ViewMediaActivity.AppBarLayout" + app:titleTextColor="@color/white" /> + + <ProgressBar + android:id="@+id/progressBarShare" + style="?android:attr/progressBarStyleLarge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:visibility="gone" /> + +</FrameLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_view_thread.xml b/app/src/main/res/layout/activity_view_thread.xml new file mode 100644 index 0000000..9db876c --- /dev/null +++ b/app/src/main/res/layout/activity_view_thread.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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" + tools:context="com.keylesspalace.tusky.components.viewthread.ViewThreadActivity"> + + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/appBar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:elevation="@dimen/actionbar_elevation" + app:elevationOverlayEnabled="false"> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + style="@style/Widget.AppCompat.Toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:contentInsetStartWithNavigation="0dp" + app:layout_scrollFlags="scroll|enterAlways" /> + + </com.google.android.material.appbar.AppBarLayout> + + <androidx.fragment.app.FragmentContainerView + android:id="@+id/fragment_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + <include layout="@layout/item_status_bottom_sheet" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/card_license.xml b/app/src/main/res/layout/card_license.xml new file mode 100644 index 0000000..339d48a --- /dev/null +++ b/app/src/main/res/layout/card_license.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + tools:layout_height="wrap_content" + tools:layout_width="match_parent" + tools:parentTag="com.google.android.material.card.MaterialCardView"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackgroundBorderless" + android:orientation="vertical" + android:padding="8dp"> + + <TextView + android:id="@+id/licenseCardName" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:fontFamily="sans-serif-medium" + android:textColor="?android:attr/textColorSecondary" /> + + <TextView + android:id="@+id/licenseCardLicense" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?android:attr/textColorTertiary" /> + + <TextView + android:id="@+id/licenseCardLink" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?attr/colorPrimary" /> + </LinearLayout> + +</merge> diff --git a/app/src/main/res/layout/dialog_add_poll.xml b/app/src/main/res/layout/dialog_add_poll.xml new file mode 100644 index 0000000..4ba7225 --- /dev/null +++ b/app/src/main/res/layout/dialog_add_poll.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingStart="16dp" + android:paddingTop="8dp" + android:paddingEnd="16dp"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/pollChoices" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:nestedScrollingEnabled="false" + android:overScrollMode="never" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + app:layout_constraintTop_toTopOf="parent" /> + + <Button + android:id="@+id/addChoiceButton" + style="@style/TuskyButton.Outlined" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginEnd="8dp" + android:text="@string/add_poll_choice" + app:layout_constraintEnd_toStartOf="@id/pollDurationSpinner" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/pollChoices" /> + + <androidx.appcompat.widget.AppCompatSpinner + android:id="@+id/pollDurationSpinner" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + app:layout_constraintBottom_toBottomOf="@id/addChoiceButton" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/addChoiceButton" + app:layout_constraintTop_toTopOf="@id/addChoiceButton" /> + + <CheckBox + android:id="@+id/multipleChoicesCheckBox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:text="@string/poll_allow_multiple_choices" + app:buttonTint="@color/compound_button_color" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/addChoiceButton" /> + + </androidx.constraintlayout.widget.ConstraintLayout> +</androidx.core.widget.NestedScrollView> diff --git a/app/src/main/res/layout/dialog_filter.xml b/app/src/main/res/layout/dialog_filter.xml new file mode 100644 index 0000000..376167d --- /dev/null +++ b/app/src/main/res/layout/dialog_filter.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="24dp"> + + <EditText + android:id="@+id/phraseEditText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/filter_add_description" + android:inputType="text" + android:importantForAutofill="no" + app:layout_constraintTop_toTopOf="parent" + /> + <CheckBox + android:id="@+id/phraseWholeWord" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/filter_dialog_whole_word" + app:layout_constraintTop_toBottomOf="@id/phraseEditText" + app:layout_constraintLeft_toLeftOf="parent" + /> + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:lineSpacingMultiplier="1.1" + app:layout_constraintTop_toBottomOf="@id/phraseWholeWord" + app:layout_constraintLeft_toLeftOf="parent" + android:text="@string/filter_dialog_whole_word_description" + /> +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_focus.xml b/app/src/main/res/layout/dialog_focus.xml new file mode 100644 index 0000000..c4eeb96 --- /dev/null +++ b/app/src/main/res/layout/dialog_focus.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <TextView + android:id="@+id/focusExplanation" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center_horizontal" + android:padding="24dp" + android:gravity="center" + android:textSize="18sp" + android:text="@string/set_focus_description" /> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:id="@+id/imageContainer"> + + <!-- todo add padding --> + <ImageView + android:id="@+id/imageView" + android:contentDescription="@string/label_image" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + <com.keylesspalace.tusky.components.compose.view.FocusIndicatorView + android:id="@+id/focusIndicator" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + </FrameLayout> + +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_follow_hashtag.xml b/app/src/main/res/layout/dialog_follow_hashtag.xml new file mode 100644 index 0000000..cdcf302 --- /dev/null +++ b/app/src/main/res/layout/dialog_follow_hashtag.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <!-- textNoSuggestions is to disable spell check, it will auto-complete --> + <AutoCompleteTextView + android:id="@+id/hashtag" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:inputType="textNoSuggestions" + android:hint="@string/dialog_follow_hashtag_hint" /> + +</LinearLayout> diff --git a/app/src/main/res/layout/dialog_image_description.xml b/app/src/main/res/layout/dialog_image_description.xml new file mode 100644 index 0000000..e44b488 --- /dev/null +++ b/app/src/main/res/layout/dialog_image_description.xml @@ -0,0 +1,65 @@ +<?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:orientation="vertical" + android:paddingBottom="0dp"> + + <com.ortiz.touchview.TouchImageView + android:id="@+id/imageDescriptionView" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:contentDescription="@string/post_media_image" /> + + <com.google.android.material.textfield.TextInputLayout + style="@style/TuskyTextInput" + + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:layout_marginStart="?dialogPreferredPadding" + android:layout_marginEnd="?dialogPreferredPadding" + android:layout_marginTop="?dialogPreferredPadding" + app:counterEnabled="false" + app:counterTextColor="?android:textColorTertiary" + app:hintEnabled="false"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/imageDescriptionText" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="start" + tools:hint="Description" + android:importantForAutofill="no" + android:inputType="textCapSentences|textMultiLine|textAutoCorrect" /> + + </com.google.android.material.textfield.TextInputLayout> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="0" + android:orientation="horizontal" + android:gravity="end" + android:layout_marginStart="?dialogPreferredPadding" + android:layout_marginEnd="?dialogPreferredPadding"> + + <Button + android:id="@+id/cancelButton" + style="@style/TuskyButton.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@android:string/cancel" /> + + <Button + android:id="@+id/okButton" + style="@style/TuskyButton.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@android:string/ok" /> + </LinearLayout> +</LinearLayout> diff --git a/app/src/main/res/layout/dialog_list.xml b/app/src/main/res/layout/dialog_list.xml new file mode 100644 index 0000000..238fc77 --- /dev/null +++ b/app/src/main/res/layout/dialog_list.xml @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <EditText + android:id="@+id/nameText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:layout_marginBottom="0dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:inputType="text" + android:hint="@string/hint_list_name" + android:importantForAutofill="no" + /> + + <CheckBox + android:id="@+id/exclusiveCheckbox" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="0dp" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:layout_marginBottom="0dp" + app:layout_constraintTop_toBottomOf="@id/nameText" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:text="@string/list_exclusive_label" + /> + + <TextView + android:id="@+id/replyPolicyLabel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_constraintTop_toBottomOf="@id/exclusiveCheckbox" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + android:layout_marginTop="8dp" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:layout_marginBottom="0dp" + android:text="@string/list_reply_policy_label" + /> + + <Spinner + android:id="@+id/replyPolicySpinner" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintTop_toBottomOf="@id/replyPolicyLabel" + app:layout_constraintStart_toStartOf="parent" + android:layout_marginTop="0dp" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:layout_marginBottom="0dp" + android:entries="@array/list_reply_policies_display" + /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/dialog_mute_account.xml b/app/src/main/res/layout/dialog_mute_account.xml new file mode 100644 index 0000000..e826445 --- /dev/null +++ b/app/src/main/res/layout/dialog_mute_account.xml @@ -0,0 +1,37 @@ +<?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_height="match_parent" + android:layout_width="match_parent" + android:orientation="vertical" + android:paddingTop="20dp" + android:paddingLeft="20dp" + android:paddingRight="20dp"> + + <TextView android:id="@+id/warning" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:paddingBottom="20dp" + tools:text="@string/dialog_mute_warning"/> + + <CheckBox android:id="@+id/checkbox" + android:layout_height="wrap_content" + android:layout_width="wrap_content" + android:textColor="@color/textColorTertiary" + app:buttonTint="@color/compound_button_color" + android:text="@string/dialog_mute_hide_notifications"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingTop="20dp" + android:text="@string/label_duration" /> + + <Spinner + android:id="@+id/duration" + android:entries="@array/mute_duration_names" + android:layout_width="wrap_content" + android:layout_height="wrap_content" /> + +</LinearLayout> diff --git a/app/src/main/res/layout/exo_player_control_view.xml b/app/src/main/res/layout/exo_player_control_view.xml new file mode 100644 index 0000000..7fe37e9 --- /dev/null +++ b/app/src/main/res/layout/exo_player_control_view.xml @@ -0,0 +1,159 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2020 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> +<!-- This file is "magic". Simply by existing in Tusky at this path, it overrides + the media3 default layout. This version is adapted from media3-ui-1.1.0. --> +<merge xmlns:android="http://schemas.android.com/apk/res/android"> + + <!-- 0dp dimensions are used to prevent this view from influencing the size of + the parent view if it uses "wrap_content". It is expanded to occupy the + entirety of the parent in code, after the parent's size has been + determined. See: https://github.com/google/ExoPlayer/issues/8726. + --> + <!-- Change from media3 default: hide "dark curtain" background element --> + <!-- + <View android:id="@id/exo_controls_background" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="@color/exo_black_opacity_60"/> + --> + + <!-- Changes from media3 default: Background is gray rather than transparent; moved before (underneath on Z axis) exo_bottom_bar --> + <LinearLayout + android:id="@id/exo_center_controls" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:background="@color/exo_bottom_bar_background" + android:gravity="center" + android:padding="@dimen/exo_styled_controls_padding" + android:clipToPadding="false" + android:layoutDirection="ltr"> + + <ImageButton android:id="@id/exo_prev" + style="@style/ExoStyledControls.Button.Center.Previous"/> + + <include layout="@layout/exo_player_control_rewind_button" /> + + <!-- Changes from media3 default: Different style to add more margins --> + <ImageButton android:id="@id/exo_play_pause" + style="@style/TuskyExoPlayerPlayPause"/> + + <include layout="@layout/exo_player_control_ffwd_button" /> + + <ImageButton android:id="@id/exo_next" + style="@style/ExoStyledControls.Button.Center.Next"/> + + </LinearLayout> + + <FrameLayout android:id="@id/exo_bottom_bar" + android:layout_width="match_parent" + android:layout_height="@dimen/exo_styled_bottom_bar_height" + android:layout_marginTop="@dimen/exo_styled_bottom_bar_margin_top" + android:layout_gravity="bottom" + android:background="@color/exo_bottom_bar_background" + android:layoutDirection="ltr"> + + <LinearLayout android:id="@id/exo_time" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingStart="@dimen/exo_styled_bottom_bar_time_padding" + android:paddingEnd="@dimen/exo_styled_bottom_bar_time_padding" + android:paddingLeft="@dimen/exo_styled_bottom_bar_time_padding" + android:paddingRight="@dimen/exo_styled_bottom_bar_time_padding" + android:layout_gravity="center_vertical|start" + android:layoutDirection="ltr"> + + <TextView android:id="@id/exo_position" + style="@style/ExoStyledControls.TimeText.Position"/> + + <TextView + style="@style/ExoStyledControls.TimeText.Separator"/> + + <TextView android:id="@id/exo_duration" + style="@style/ExoStyledControls.TimeText.Duration"/> + + </LinearLayout> + + <LinearLayout android:id="@id/exo_basic_controls" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical|end" + android:layoutDirection="ltr"> + + <ImageButton android:id="@id/exo_vr" + style="@style/ExoStyledControls.Button.Bottom.VR"/> + + <ImageButton android:id="@id/exo_shuffle" + style="@style/ExoStyledControls.Button.Bottom.Shuffle"/> + + <ImageButton android:id="@id/exo_repeat_toggle" + style="@style/ExoStyledControls.Button.Bottom.RepeatToggle"/> + + <ImageButton android:id="@id/exo_subtitle" + style="@style/ExoStyledControls.Button.Bottom.CC"/> + + <ImageButton android:id="@id/exo_settings" + style="@style/ExoStyledControls.Button.Bottom.Settings"/> + + <ImageButton android:id="@id/exo_fullscreen" + style="@style/ExoStyledControls.Button.Bottom.FullScreen"/> + + <ImageButton android:id="@id/exo_overflow_show" + style="@style/ExoStyledControls.Button.Bottom.OverflowShow"/> + + </LinearLayout> + + <HorizontalScrollView android:id="@id/exo_extra_controls_scroll_view" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical|end" + android:visibility="invisible"> + + <LinearLayout android:id="@id/exo_extra_controls" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layoutDirection="ltr"> + + <ImageButton android:id="@id/exo_overflow_hide" + style="@style/ExoStyledControls.Button.Bottom.OverflowHide"/> + + </LinearLayout> + + </HorizontalScrollView> + + </FrameLayout> + + <View android:id="@id/exo_progress_placeholder" + android:layout_width="match_parent" + android:layout_height="@dimen/exo_styled_progress_layout_height" + android:layout_gravity="bottom" + android:layout_marginBottom="@dimen/exo_styled_progress_margin_bottom"/> + + <LinearLayout android:id="@id/exo_minimal_controls" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="bottom|end" + android:layout_marginBottom="@dimen/exo_styled_minimal_controls_margin_bottom" + android:orientation="horizontal" + android:gravity="center_vertical" + android:layoutDirection="ltr"> + + <ImageButton android:id="@id/exo_minimal_fullscreen" + style="@style/ExoStyledControls.Button.Bottom.FullScreen"/> + + </LinearLayout> + +</merge> diff --git a/app/src/main/res/layout/fragment_account_list.xml b/app/src/main/res/layout/fragment_account_list.xml new file mode 100644 index 0000000..b4f9df5 --- /dev/null +++ b/app/src/main/res/layout/fragment_account_list.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.keylesspalace.tusky.view.TuskySwipeRefreshLayout + android:id="@+id/swipeRefreshLayout" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="@dimen/recyclerview_bottom_padding_no_actionbutton" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/messageView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:visibility="gone" + tools:visibility="visible" /> + </FrameLayout> + </com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> + + +</FrameLayout> diff --git a/app/src/main/res/layout/fragment_accounts_in_list.xml b/app/src/main/res/layout/fragment_accounts_in_list.xml new file mode 100644 index 0000000..d996560 --- /dev/null +++ b/app/src/main/res/layout/fragment_accounts_in_list.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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:animateLayoutChanges="true"> + + <androidx.appcompat.widget.SearchView + android:id="@+id/searchView" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:imeOptions="actionSearch" + android:lines="1" + app:closeIcon="@drawable/ic_close_24dp" + app:defaultQueryHint="@string/hint_search_people_list" + app:iconifiedByDefault="false" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/accountsRecycler" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginTop="8dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/searchView" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/messageView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:src="@android:color/transparent" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/errorphant_error" + tools:visibility="visible" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/accountsSearchRecycler" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginTop="8dp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/searchView" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_domain_blocks.xml b/app/src/main/res/layout/fragment_domain_blocks.xml new file mode 100644 index 0000000..ec60889 --- /dev/null +++ b/app/src/main/res/layout/fragment_domain_blocks.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="@dimen/recyclerview_bottom_padding_no_actionbutton" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/messageView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:visibility="gone" + tools:visibility="visible" /> +</FrameLayout> diff --git a/app/src/main/res/layout/fragment_lists_list.xml b/app/src/main/res/layout/fragment_lists_list.xml new file mode 100644 index 0000000..64c87db --- /dev/null +++ b/app/src/main/res/layout/fragment_lists_list.xml @@ -0,0 +1,36 @@ +<?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" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + android:layout_gravity="center" + tools:visibility="gone" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/listsView" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" + android:visibility="gone" + tools:visibility="visible" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/messageView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:src="@android:color/transparent" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/errorphant_error" + tools:visibility="visible" /> +</FrameLayout> diff --git a/app/src/main/res/layout/fragment_report_done.xml b/app/src/main/res/layout/fragment_report_done.xml new file mode 100644 index 0000000..5eab9b1 --- /dev/null +++ b/app/src/main/res/layout/fragment_report_done.xml @@ -0,0 +1,108 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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" + tools:context=".components.report.fragments.ReportStatusesFragment"> + + <View + android:id="@+id/checkMark" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginTop="56dp" + android:background="@drawable/report_success_background" + app:layout_constraintDimensionRatio="H,1:1" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_default="percent" + app:layout_constraintWidth_percent="0.35" /> + + <ImageView + android:layout_width="0dp" + android:layout_height="0dp" + android:scaleType="fitCenter" + app:layout_constraintBottom_toBottomOf="@id/checkMark" + app:srcCompat="@drawable/ic_check_24dp" + app:layout_constraintEnd_toEndOf="@id/checkMark" + app:layout_constraintHeight_percent="0.3" + app:layout_constraintStart_toStartOf="@id/checkMark" + app:layout_constraintTop_toTopOf="@id/checkMark" + app:layout_constraintWidth_percent="0.22" + tools:ignore="ContentDescription" /> + + <TextView + android:id="@+id/textReported" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="48dp" + android:gravity="center_horizontal" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_large" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/checkMark" + app:layout_constraintWidth_percent="0.9" /> + + <Button + android:id="@+id/buttonMute" + style="@style/TuskyButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="@dimen/min_report_button_width" + android:text="@string/action_mute" + app:layout_constraintBottom_toTopOf="@id/buttonBlock" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/textReported" + app:layout_constraintVertical_chainStyle="packed" /> + + <ProgressBar + android:id="@+id/progressMute" + style="?android:attr/progressBarStyleSmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="@id/buttonMute" + app:layout_constraintEnd_toEndOf="@id/buttonMute" + app:layout_constraintStart_toStartOf="@id/buttonMute" + app:layout_constraintTop_toTopOf="@id/buttonMute" /> + + <Button + android:id="@+id/buttonBlock" + style="@style/TuskyButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minWidth="@dimen/min_report_button_width" + android:text="@string/action_block" + app:layout_constraintBottom_toTopOf="@id/buttonDone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/buttonMute" + app:layout_constraintVertical_chainStyle="packed" /> + + <ProgressBar + android:id="@+id/progressBlock" + style="?android:attr/progressBarStyleSmall" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="@id/buttonBlock" + app:layout_constraintEnd_toEndOf="@id/buttonBlock" + app:layout_constraintStart_toStartOf="@id/buttonBlock" + app:layout_constraintTop_toTopOf="@id/buttonBlock" /> + + <Button + android:id="@+id/buttonDone" + style="@style/TuskyButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="32dp" + android:minWidth="@dimen/min_report_button_width" + android:text="@string/button_done" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/buttonBlock" + app:layout_constraintVertical_chainStyle="packed" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_report_note.xml b/app/src/main/res/layout/fragment_report_note.xml new file mode 100644 index 0000000..047fa9b --- /dev/null +++ b/app/src/main/res/layout/fragment_report_note.xml @@ -0,0 +1,130 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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" + tools:context=".components.report.fragments.ReportStatusesFragment"> + + <ScrollView + android:layout_width="match_parent" + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@id/buttonReport" + app:layout_constraintTop_toTopOf="parent"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideBegin" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_begin="16dp" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideEnd" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_end="16dp" /> + + <TextView + android:id="@+id/reportDescription" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="@string/report_description_1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_small" + app:layout_constraintEnd_toEndOf="@id/guideEnd" + app:layout_constraintStart_toStartOf="@id/guideBegin" + android:lineSpacingMultiplier="1.1" + app:layout_constraintTop_toTopOf="parent" /> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/layoutAdditionalInfo" + style="@style/TuskyTextInput" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:hint="@string/hint_additional_info" + app:hintEnabled="true" + app:layout_constraintEnd_toEndOf="@id/guideEnd" + app:layout_constraintStart_toStartOf="@id/guideBegin" + app:layout_constraintTop_toBottomOf="@id/reportDescription"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/editNote" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="top" + android:inputType="textCapSentences|textMultiLine" + android:minLines="4" /> + + </com.google.android.material.textfield.TextInputLayout> + + <TextView + android:id="@+id/reportDescriptionRemoteInstance" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="@string/report_description_remote_instance" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_small" + android:lineSpacingMultiplier="1.1" + app:layout_constraintEnd_toEndOf="@id/guideEnd" + app:layout_constraintStart_toStartOf="@id/guideBegin" + app:layout_constraintTop_toBottomOf="@id/layoutAdditionalInfo" /> + + + <CheckBox + android:id="@+id/checkIsNotifyRemote" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:text="@string/report_remote_instance" + android:textSize="?attr/status_text_medium" + app:buttonTint="@color/compound_button_color" + app:layout_constraintEnd_toEndOf="@id/guideEnd" + app:layout_constraintStart_toStartOf="@id/guideBegin" + app:layout_constraintTop_toBottomOf="@id/reportDescriptionRemoteInstance" /> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="48dp" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/checkIsNotifyRemote" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + </ScrollView> + + <Button + android:id="@+id/buttonBack" + tools:ignore="BackButton" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="16dp" + android:text="@string/button_back" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/buttonReport" /> + + <Button + android:id="@+id/buttonReport" + style="@style/TuskyButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="16dp" + android:text="@string/action_report" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_report_statuses.xml b/app/src/main/res/layout/fragment_report_statuses.xml new file mode 100644 index 0000000..1646387 --- /dev/null +++ b/app/src/main/res/layout/fragment_report_statuses.xml @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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" + tools:context=".components.report.fragments.ReportStatusesFragment"> + + <com.keylesspalace.tusky.view.TuskySwipeRefreshLayout + android:id="@+id/swipeRefreshLayout" + android:layout_width="match_parent" + android:layout_height="0dp" + app:layout_constraintBottom_toTopOf="@id/buttonContinue" + app:layout_constraintTop_toTopOf="parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + + </com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> + + <ProgressBar + android:id="@+id/progressBarTop" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:indeterminate="true" + app:layout_constraintTop_toTopOf="@id/swipeRefreshLayout" /> + + <ProgressBar + android:id="@+id/progressBarBottom" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:indeterminate="true" + app:layout_constraintBottom_toBottomOf="@id/swipeRefreshLayout" /> + + <ProgressBar + android:id="@+id/progressBarLoading" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:indeterminate="true" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="@id/swipeRefreshLayout" + app:layout_constraintEnd_toEndOf="@id/swipeRefreshLayout" + app:layout_constraintStart_toStartOf="@id/swipeRefreshLayout" + app:layout_constraintTop_toTopOf="@id/swipeRefreshLayout" /> + + <Button + android:id="@+id/buttonCancel" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="16dp" + android:text="@android:string/cancel" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintEnd_toStartOf="@id/buttonContinue" /> + + <Button + android:id="@+id/buttonContinue" + style="@style/TuskyButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="16dp" + android:text="@string/button_continue" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_search.xml b/app/src/main/res/layout/fragment_search.xml new file mode 100644 index 0000000..0827b52 --- /dev/null +++ b/app/src/main/res/layout/fragment_search.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="@dimen/timeline_width" + android:layout_height="match_parent"> + + <com.keylesspalace.tusky.view.TuskySwipeRefreshLayout + android:id="@+id/swipeRefreshLayout" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/searchRecyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:background="?android:attr/windowBackground" + android:clipToPadding="false" + android:paddingBottom="@dimen/recyclerview_bottom_padding_no_actionbutton" + tools:listitem="@layout/item_account" /> + + </com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> + + <ProgressBar + android:id="@+id/searchProgressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:indeterminate="true" + android:visibility="gone" /> + + <TextView + android:id="@+id/searchNoResultsText" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:text="@string/search_no_results" + android:visibility="gone" /> + + <ProgressBar + android:id="@+id/progressBarBottom" + style="?android:attr/progressBarStyleHorizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:indeterminate="true" + android:visibility="gone" /> + +</FrameLayout> + diff --git a/app/src/main/res/layout/fragment_timeline.xml b/app/src/main/res/layout/fragment_timeline.xml new file mode 100644 index 0000000..9f13ccd --- /dev/null +++ b/app/src/main/res/layout/fragment_timeline.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?android:attr/colorBackground"> + + <com.keylesspalace.tusky.view.TuskySwipeRefreshLayout + android:id="@+id/swipeRefreshLayout" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerView" + android:clipToPadding="false" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/statusView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginBottom="?attr/actionBarSize" + android:visibility="gone" /> + </FrameLayout> + </com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <androidx.core.widget.ContentLoadingProgressBar + android:id="@+id/topProgressBar" + style="@style/Widget.AppCompat.ProgressBar.Horizontal" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:indeterminate="true" + android:visibility="gone" + app:layout_constraintBottom_toTopOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_timeline_notifications.xml b/app/src/main/res/layout/fragment_timeline_notifications.xml new file mode 100644 index 0000000..46c5e06 --- /dev/null +++ b/app/src/main/res/layout/fragment_timeline_notifications.xml @@ -0,0 +1,99 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright 2023 Tusky Contributors + ~ + ~ This file is a part of Tusky. + ~ + ~ This program is free software; you can redistribute it and/or modify it under the terms of the + ~ GNU General Public License as published by the Free Software Foundation; either version 3 of the + ~ License, or (at your option) any later version. + ~ + ~ Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + ~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + ~ Public License for more details. + ~ + ~ You should have received a copy of the GNU General Public License along with Tusky; if not, + ~ see <http://www.gnu.org/licenses>. + --> + +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?android:attr/colorBackground"> + + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/appBarOptions" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?attr/colorSurface" + app:elevation="0dp"> + + <LinearLayout + android:id="@+id/topButtonsLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <Button + android:id="@+id/buttonClear" + style="@style/TuskyButton.TextButton" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/notifications_clear" + android:textSize="?attr/status_text_medium" /> + + <Button + android:id="@+id/buttonFilter" + style="@style/TuskyButton.TextButton" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:text="@string/notifications_apply_filter" + android:textSize="?attr/status_text_medium" /> + + </LinearLayout> + + <View + android:layout_width="match_parent" + android:layout_height="1dp" + android:layout_gravity="bottom" + android:background="?android:attr/listDivider" + app:layout_scrollFlags="scroll|enterAlways" /> + + </com.google.android.material.appbar.AppBarLayout> + + <com.keylesspalace.tusky.view.TuskySwipeRefreshLayout + android:id="@+id/swipeRefreshLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:layout_behavior="@string/appbar_scrolling_view_behavior"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="@dimen/recyclerview_bottom_padding_actionbutton" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/statusView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginBottom="?attr/actionBarSize" + android:visibility="gone" /> + </FrameLayout> + + </com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/fragment_trending_tags.xml b/app/src/main/res/layout/fragment_trending_tags.xml new file mode 100644 index 0000000..c736764 --- /dev/null +++ b/app/src/main/res/layout/fragment_trending_tags.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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="?android:attr/colorBackground"> + + <com.keylesspalace.tusky.view.TuskySwipeRefreshLayout + android:id="@+id/swipeRefreshLayout" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="@dimen/recyclerview_bottom_padding_no_actionbutton" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/messageView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:visibility="gone" + tools:visibility="visible" /> + </FrameLayout> + </com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_view_edits.xml b/app/src/main/res/layout/fragment_view_edits.xml new file mode 100644 index 0000000..ee1e53d --- /dev/null +++ b/app/src/main/res/layout/fragment_view_edits.xml @@ -0,0 +1,115 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout 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="?android:attr/colorBackground"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <androidx.constraintlayout.widget.ConstraintLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <ImageView + android:id="@+id/status_avatar" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="16dp" + android:layout_marginTop="16dp" + android:contentDescription="@string/action_view_profile" + android:importantForAccessibility="no" + android:scaleType="centerCrop" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/avatar_default" /> + + <ImageView + android:id="@+id/status_avatar_inset" + android:layout_width="24dp" + android:layout_height="24dp" + android:contentDescription="@null" + android:importantForAccessibility="no" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="@id/status_avatar" + app:layout_constraintEnd_toEndOf="@id/status_avatar" + tools:src="#000" + tools:visibility="visible" /> + + <TextView + android:id="@+id/status_display_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:ellipsize="end" + android:importantForAccessibility="no" + android:maxLines="1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_large" + android:textStyle="normal|bold" + app:layout_constrainedWidth="true" + app:layout_constraintStart_toEndOf="@id/status_avatar" + app:layout_constraintTop_toTopOf="@+id/status_avatar" + tools:ignore="SelectableText" + tools:text="Ente r the void you foooooo" /> + + <TextView + android:id="@+id/status_username" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:ellipsize="end" + android:importantForAccessibility="no" + android:maxLines="1" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:layout_constraintStart_toStartOf="@+id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_display_name" + tools:ignore="SelectableText" + tools:text="\@Entenhausen@birbsarecooooooooooool.site" /> + </androidx.constraintlayout.widget.ConstraintLayout> + + <com.keylesspalace.tusky.view.TuskySwipeRefreshLayout + android:id="@+id/swipeRefreshLayout" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_marginTop="8dp" + android:layout_weight="1" + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/status_avatar" + app:layout_constraintVertical_weight="1"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clipToPadding="false" + android:paddingBottom="@dimen/recyclerview_bottom_padding_no_actionbutton" + android:scrollbarStyle="outsideInset" + android:scrollbars="vertical" /> + </com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> + </LinearLayout> + + <ProgressBar + android:id="@+id/initialProgressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:contentDescription="@string/a11y_label_loading_thread" + android:indeterminate="true" + android:visibility="gone" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/statusView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:visibility="gone"> + + </com.keylesspalace.tusky.view.BackgroundMessageView> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/fragment_view_image.xml b/app/src/main/res/layout/fragment_view_image.xml new file mode 100644 index 0000000..0dbd233 --- /dev/null +++ b/app/src/main/res/layout/fragment_view_image.xml @@ -0,0 +1,79 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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:clickable="true" + android:focusable="true"> + + <com.ortiz.touchview.TouchImageView + android:id="@+id/photoView" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerHorizontal="true" + android:layout_centerVertical="true" + android:layout_gravity="center" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <!-- This should be inside CoordinatorLayout for two reasons: + + 1. TouchImageView really wants some constraints ans has no size otherwise + 2. We don't want sheet to overlap with appbar but the only way to do it with autosizing + is to gibe parent some margin. --> + <androidx.coordinatorlayout.widget.CoordinatorLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_marginTop="70dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <LinearLayout + android:id="@+id/captionSheet" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="@drawable/description_bg_expanded" + android:orientation="vertical" + app:behavior_peekHeight="90dp" + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"> + + <View + android:layout_width="24dp" + android:layout_height="3dp" + android:layout_gravity="center_horizontal" + android:layout_marginTop="10dp" + android:layout_marginBottom="10dp" + android:background="@drawable/ic_drag_indicator_horiz_24dp" + android:importantForAccessibility="no" /> + + <androidx.core.widget.NestedScrollView + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/mediaDescription" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hyphenationFrequency="full" + android:lineSpacingMultiplier="1.1" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingBottom="8dp" + android:textColor="?android:textColorPrimary" + android:textIsSelectable="true" + android:textSize="?attr/status_text_medium" + tools:text="Some media description which might get quite long so that it won't easily fit in one line" /> + </androidx.core.widget.NestedScrollView> + </LinearLayout> + </androidx.coordinatorlayout.widget.CoordinatorLayout> +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/fragment_view_thread.xml b/app/src/main/res/layout/fragment_view_thread.xml new file mode 100644 index 0000000..63e17eb --- /dev/null +++ b/app/src/main/res/layout/fragment_view_thread.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <com.google.android.material.progressindicator.LinearProgressIndicator + android:id="@+id/threadProgressBar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:contentDescription="@string/a11y_label_loading_thread" + android:indeterminate="true" + android:visibility="gone" /> + + <com.keylesspalace.tusky.view.TuskySwipeRefreshLayout + android:id="@+id/swipeRefreshLayout" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior"> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/recyclerView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?android:attr/colorBackground" + android:clipToPadding="false" + android:paddingBottom="@dimen/recyclerview_bottom_padding_no_actionbutton" + android:scrollbarStyle="outsideInset" + android:scrollbars="vertical" /> + + <com.keylesspalace.tusky.view.BackgroundMessageView + android:id="@+id/statusView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:visibility="gone" /> + </FrameLayout> + </com.keylesspalace.tusky.view.TuskySwipeRefreshLayout> + </LinearLayout> + + <ProgressBar + android:id="@+id/initialProgressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:contentDescription="@string/a11y_label_loading_thread" + android:indeterminate="true" + android:visibility="gone" /> + +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/layout/fragment_view_video.xml b/app/src/main/res/layout/fragment_view_video.xml new file mode 100644 index 0000000..728dda2 --- /dev/null +++ b/app/src/main/res/layout/fragment_view_video.xml @@ -0,0 +1,50 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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/videoContainer" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:clickable="true" + android:focusable="true"> + + <TextView + android:id="@+id/mediaDescription" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="?attr/actionBarSize" + android:background="#60000000" + android:hyphenationFrequency="full" + android:lineSpacingMultiplier="1.1" + android:padding="8dp" + android:textAlignment="center" + android:textColor="#eee" + android:textSize="?attr/status_text_medium" + android:scrollbars="vertical" + app:layout_constraintTop_toTopOf="parent" + tools:text="Some media description" /> + + <androidx.media3.ui.PlayerView + android:id="@+id/videoView" + android:layout_width="wrap_content" + android:layout_height="match_parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:use_controller="false" + app:show_previous_button="false" + app:show_next_button="false" + app:show_timeout="0" + app:hide_on_touch="false" /> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_account.xml b/app/src/main/res/layout/item_account.xml new file mode 100644 index 0000000..397579d --- /dev/null +++ b/app/src/main/res/layout/item_account.xml @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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/account_container" + android:layout_width="match_parent" + android:layout_height="72dp" + android:background="?attr/selectableItemBackground" + android:paddingStart="16dp" + android:paddingEnd="16dp"> + + <ImageView + android:id="@+id/account_avatar" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginEnd="24dp" + android:contentDescription="@string/action_view_profile" + android:foregroundGravity="center_vertical" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/avatar_default" /> + + <ImageView + android:id="@+id/account_bot_badge" + android:layout_width="24dp" + android:layout_height="24dp" + android:contentDescription="@null" + android:importantForAccessibility="no" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="@id/account_avatar" + app:layout_constraintEnd_toEndOf="@id/account_avatar" + app:srcCompat="@drawable/bot_badge" + tools:src="#000" + tools:visibility="visible" /> + + <TextView + android:id="@+id/account_display_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_large" + android:textStyle="normal|bold" + app:layout_constraintBottom_toTopOf="@id/account_username" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/account_avatar" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" + tools:text="Display name" /> + + <TextView + android:id="@+id/account_username" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/account_avatar" + app:layout_constraintTop_toBottomOf="@id/account_display_name" + tools:text="\@username" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/item_account_field.xml b/app/src/main/res/layout/item_account_field.xml new file mode 100644 index 0000000..17ea0db --- /dev/null +++ b/app/src/main/res/layout/item_account_field.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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:paddingTop="4dp"> + + <!-- 30% width for the field name, 70% for the value --> + <TextView + android:id="@+id/accountFieldName" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:fontFamily="sans-serif-medium" + android:gravity="center" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + android:textDirection="firstStrong" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_percent=".3" + tools:text="Field title" /> + + <TextView + android:id="@+id/accountFieldValue" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="12dp" + android:drawablePadding="6dp" + android:gravity="center" + android:lineSpacingMultiplier="1.1" + android:textIsSelectable="true" + android:textSize="?attr/status_text_medium" + android:textDirection="firstStrong" + app:layout_constrainedWidth="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/accountFieldName" + app:layout_constraintTop_toTopOf="parent" + tools:text="Field content. This can contain links and custom emojis" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_account_media.xml b/app/src/main/res/layout/item_account_media.xml new file mode 100644 index 0000000..c82dacc --- /dev/null +++ b/app/src/main/res/layout/item_account_media.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools"> + + <com.keylesspalace.tusky.components.account.media.SquareImageView + android:id="@+id/accountMediaImageView" + android:layout_width="match_parent" + android:layout_height="0dp" + android:scaleType="centerCrop" /> + + <ImageView + android:id="@+id/accountMediaImageViewOverlay" + tools:ignore="ContentDescription" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" /> + +</FrameLayout> diff --git a/app/src/main/res/layout/item_add_poll_option.xml b/app/src/main/res/layout/item_add_poll_option.xml new file mode 100644 index 0000000..ad91f84 --- /dev/null +++ b/app/src/main/res/layout/item_add_poll_option.xml @@ -0,0 +1,33 @@ +<?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" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/optionTextInputLayout" + style="@style/TuskyTextInput" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_weight="1"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/optionEditText" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + </com.google.android.material.textfield.TextInputLayout> + + <ImageButton + android:id="@+id/deleteButton" + style="@style/TuskyImageButton" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_gravity="bottom" + android:layout_marginStart="8dp" + android:layout_marginBottom="8dp" + android:contentDescription="@string/action_remove" + app:srcCompat="@drawable/ic_clear_24dp" /> + +</LinearLayout> diff --git a/app/src/main/res/layout/item_announcement.xml b/app/src/main/res/layout/item_announcement.xml new file mode 100644 index 0000000..1e137cc --- /dev/null +++ b/app/src/main/res/layout/item_announcement.xml @@ -0,0 +1,56 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/text" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:lineSpacingMultiplier="1.1" + android:padding="8dp" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <com.google.android.material.chip.ChipGroup + android:id="@+id/chipGroup" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:padding="8dp" + app:layout_constraintBottom_toTopOf="@id/announcementDate" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/text"> + + <com.google.android.material.chip.Chip + android:id="@+id/addReactionChip" + style="@style/Widget.MaterialComponents.Chip.Action" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:checkable="false" + android:contentDescription="@string/action_add_reaction" + android:textColor="?attr/colorOnPrimary" + app:chipEndPadding="4dp" + app:chipIcon="@drawable/ic_plus_24dp" + app:chipIconTint="?attr/colorOnPrimary" + app:chipSurfaceColor="?attr/colorPrimary" + app:textEndPadding="0dp" + app:textStartPadding="0dp" /> + + </com.google.android.material.chip.ChipGroup> + + <TextView + android:id="@+id/announcementDate" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:lineSpacingMultiplier="1.1" + android:paddingHorizontal="8dp" + android:paddingBottom="8dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/chipGroup" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_autocomplete_account.xml b/app/src/main/res/layout/item_autocomplete_account.xml new file mode 100644 index 0000000..c64a370 --- /dev/null +++ b/app/src/main/res/layout/item_autocomplete_account.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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:paddingStart="16dp" + android:paddingTop="8dp" + android:paddingEnd="16dp" + android:paddingBottom="8dp"> + + <ImageView + android:id="@+id/avatar" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginEnd="24dp" + android:foregroundGravity="center_vertical" + android:importantForAccessibility="no" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/avatar_default" /> + + <ImageView + android:id="@+id/avatarBadge" + android:layout_width="24dp" + android:layout_height="24dp" + android:contentDescription="@string/profile_badge_bot_text" + app:layout_constraintBottom_toBottomOf="@id/avatar" + app:layout_constraintEnd_toEndOf="@id/avatar" + app:srcCompat="@drawable/bot_badge" /> + + <TextView + android:id="@+id/displayName" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_large" + android:textStyle="normal|bold" + app:layout_constraintBottom_toTopOf="@id/username" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/avatar" + app:layout_constraintTop_toTopOf="@id/avatar" + app:layout_constraintVertical_chainStyle="packed" + tools:text="Display name" /> + + <TextView + android:id="@+id/username" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:layout_constraintBottom_toBottomOf="@id/avatar" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/avatar" + app:layout_constraintTop_toBottomOf="@id/displayName" + tools:text="\@username" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_autocomplete_emoji.xml b/app/src/main/res/layout/item_autocomplete_emoji.xml new file mode 100644 index 0000000..fbc2f5c --- /dev/null +++ b/app/src/main/res/layout/item_autocomplete_emoji.xml @@ -0,0 +1,33 @@ +<?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:gravity="center_vertical" + android:orientation="horizontal" + tools:ignore="UseCompoundDrawables"> + + <ImageView + android:id="@+id/preview" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_marginStart="16dp" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:importantForAccessibility="no" /> + + <TextView + android:id="@+id/shortcode" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_weight="1" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_large" + android:textStyle="normal|bold" + tools:text=":mastodon:" /> + +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/item_autocomplete_hashtag.xml b/app/src/main/res/layout/item_autocomplete_hashtag.xml new file mode 100644 index 0000000..fdb3625 --- /dev/null +++ b/app/src/main/res/layout/item_autocomplete_hashtag.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/hashtag" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:paddingHorizontal="16dp" + android:paddingVertical="12dp" + android:textSize="?attr/status_text_medium" + android:textStyle="normal|bold" + tools:text="#Tusky" /> diff --git a/app/src/main/res/layout/item_blocked_domain.xml b/app/src/main/res/layout/item_blocked_domain.xml new file mode 100644 index 0000000..e043e1a --- /dev/null +++ b/app/src/main/res/layout/item_blocked_domain.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout + 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="72dp" + android:gravity="center_vertical" + android:paddingLeft="16dp" + android:paddingRight="16dp" + > + + <ImageButton + android:id="@+id/blocked_domain_unblock" + style="@style/TuskyImageButton" + android:layout_width="32dp" + android:layout_height="32dp" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintRight_toRightOf="parent" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/action_unmute" + android:padding="4dp" + app:srcCompat="@drawable/ic_unmute_24dp" + /> + + <TextView + android:id="@+id/blocked_domain" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + android:gravity="center_vertical" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + tools:text="instance.domain.tld" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_blocked_user.xml b/app/src/main/res/layout/item_blocked_user.xml new file mode 100644 index 0000000..a4acf28 --- /dev/null +++ b/app/src/main/res/layout/item_blocked_user.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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="72dp" + android:gravity="center_vertical" + android:paddingStart="16dp" + android:paddingEnd="16dp"> + + <ImageView + android:id="@+id/blocked_user_avatar" + android:layout_width="48dp" + android:layout_height="48dp" + android:contentDescription="@string/action_view_profile" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/avatar_default" /> + + <ImageView + android:id="@+id/blocked_user_bot_badge" + android:layout_width="24dp" + android:layout_height="24dp" + android:contentDescription="@string/profile_badge_bot_text" + app:layout_constraintBottom_toBottomOf="@id/blocked_user_avatar" + app:layout_constraintEnd_toEndOf="@id/blocked_user_avatar" + app:srcCompat="@drawable/bot_badge" /> + + <TextView + android:id="@+id/blocked_user_display_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_large" + android:textStyle="normal|bold" + app:layout_constraintBottom_toTopOf="@id/blocked_user_username" + app:layout_constraintEnd_toStartOf="@id/blocked_user_unblock" + app:layout_constraintStart_toEndOf="@id/blocked_user_avatar" + app:layout_constraintTop_toTopOf="@id/blocked_user_avatar" + tools:text="Display name" /> + + <TextView + android:id="@+id/blocked_user_username" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:layout_constraintBottom_toBottomOf="@id/blocked_user_avatar" + app:layout_constraintEnd_toStartOf="@id/blocked_user_unblock" + app:layout_constraintStart_toEndOf="@id/blocked_user_avatar" + app:layout_constraintTop_toBottomOf="@id/blocked_user_display_name" + tools:text="\@username" /> + + <ImageButton + android:id="@+id/blocked_user_unblock" + style="@style/TuskyImageButton" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_alignParentEnd="true" + android:layout_centerVertical="true" + android:layout_marginStart="12dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/action_unblock" + android:padding="4dp" + app:layout_constraintBottom_toBottomOf="@id/blocked_user_avatar" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/blocked_user_avatar" + app:srcCompat="@drawable/ic_clear_24dp" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/item_conversation.xml b/app/src/main/res/layout/item_conversation.xml new file mode 100644 index 0000000..99f3c2e --- /dev/null +++ b/app/src/main/res/layout/item_conversation.xml @@ -0,0 +1,312 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:sparkbutton="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/status_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:clipToPadding="false" + android:paddingStart="12dp" + android:paddingEnd="14dp"> + + <TextView + android:id="@+id/conversation_name" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="2dp" + android:layout_marginTop="@dimen/status_reblogged_bar_padding_top" + android:gravity="center_vertical" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + android:textStyle="normal|bold" + app:layout_constraintLeft_toRightOf="parent" + app:layout_constraintRight_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:ignore="RtlSymmetry" + tools:text="ConnyDuck boosted" + tools:visibility="visible" /> + + <ImageView + android:id="@+id/status_avatar_2" + android:layout_width="52dp" + android:layout_height="52dp" + android:layout_marginTop="22dp" + android:background="@drawable/avatar_border" + android:contentDescription="@string/action_view_profile" + android:padding="2dp" + android:scaleType="centerCrop" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/status_avatar_1" + tools:src="@drawable/avatar_default" /> + + <ImageView + android:id="@+id/status_avatar_1" + android:layout_width="52dp" + android:layout_height="52dp" + android:layout_marginTop="22dp" + android:background="@drawable/avatar_border" + android:contentDescription="@string/action_view_profile" + android:padding="2dp" + android:scaleType="centerCrop" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/status_avatar" + tools:src="@drawable/avatar_default" /> + + <ImageView + android:id="@+id/status_avatar" + android:layout_width="52dp" + android:layout_height="52dp" + android:layout_marginTop="14dp" + android:background="@drawable/avatar_border" + android:contentDescription="@string/action_view_profile" + android:padding="2dp" + android:scaleType="centerCrop" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/conversation_name" + tools:src="@drawable/avatar_default" /> + + <ImageView + android:id="@+id/status_avatar_inset" + android:layout_width="24dp" + android:layout_height="24dp" + android:contentDescription="@null" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="@id/status_avatar" + app:layout_constraintEnd_toEndOf="@id/status_avatar" + tools:src="#000" + tools:visibility="visible" /> + + <TextView + android:id="@+id/status_display_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="10dp" + android:ellipsize="end" + android:maxLines="1" + android:paddingEnd="@dimen/status_display_name_padding_end" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + android:textStyle="normal|bold" + app:layout_constrainedWidth="true" + app:layout_constraintEnd_toStartOf="@id/status_meta_info" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toEndOf="@id/status_avatar" + app:layout_constraintTop_toBottomOf="@id/conversation_name" + tools:text="Ente r the void you foooooo" /> + + <TextView + android:id="@+id/status_username" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toStartOf="@id/status_meta_info" + app:layout_constraintStart_toEndOf="@id/status_display_name" + app:layout_constraintTop_toTopOf="@id/status_display_name" + tools:text="\@Entenhausen@birbsarecooooooooooool.site" /> + + <TextView + android:id="@+id/status_meta_info" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="4dp" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/status_display_name" + tools:text="13:37" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/status_content_warning_description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_display_name" + tools:text="content warning which is very long and it doesn't fit" + tools:visibility="visible" /> + + <Button + android:id="@+id/status_content_warning_button" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:minWidth="150dp" + android:minHeight="0dp" + android:paddingLeft="16dp" + android:paddingTop="4dp" + android:paddingRight="16dp" + android:paddingBottom="4dp" + android:textAllCaps="true" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_content_warning_description" + tools:text="@string/post_content_warning_show_more" + tools:visibility="visible" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/status_content" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:focusable="true" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/status_content_warning_button" + app:layout_constraintTop_toBottomOf="@id/status_content_warning_button" + tools:text="This is a status" /> + + <Button + android:id="@+id/button_toggle_content" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:minWidth="150dp" + android:minHeight="0dp" + android:paddingLeft="16dp" + android:paddingTop="4dp" + android:paddingRight="16dp" + android:paddingBottom="4dp" + android:textAllCaps="true" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_content" + tools:text="@string/post_content_show_less" + tools:visibility="visible" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/status_media_preview_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/status_media_preview_margin_top" + android:background="@drawable/media_preview_outline" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/button_toggle_content" + tools:visibility="gone"> + + <include layout="@layout/item_media_preview" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/status_poll_options" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:nestedScrollingEnabled="false" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_media_preview_container" /> + + <Button + android:id="@+id/status_poll_button" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:gravity="center" + android:minWidth="150dp" + android:minHeight="0dp" + android:paddingLeft="16dp" + android:paddingTop="4dp" + android:paddingRight="16dp" + android:paddingBottom="4dp" + android:text="@string/poll_vote" + android:textSize="?attr/status_text_medium" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_poll_options" /> + + <TextView + android:id="@+id/status_poll_description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_poll_button" + tools:text="7 votes • 7 hours remaining" /> + + <ImageButton + android:id="@+id/status_reply" + style="@style/TuskyImageButton" + android:layout_width="30dp" + android:layout_height="30dp" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:contentDescription="@string/action_reply" + android:padding="4dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/status_favourite" + app:layout_constraintHorizontal_chainStyle="spread_inside" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_poll_description" + app:srcCompat="@drawable/ic_reply_24dp" /> + + <at.connyduck.sparkbutton.SparkButton + android:id="@+id/status_favourite" + android:layout_width="30dp" + android:layout_height="30dp" + android:clipToPadding="false" + android:contentDescription="@string/action_favourite" + android:padding="4dp" + app:layout_constraintEnd_toStartOf="@id/status_bookmark" + app:layout_constraintStart_toEndOf="@id/status_reply" + app:layout_constraintTop_toTopOf="@id/status_reply" + sparkbutton:activeImage="@drawable/ic_favourite_active_24dp" + sparkbutton:iconSize="28dp" + sparkbutton:inactiveImage="@drawable/ic_favourite_24dp" + sparkbutton:primaryColor="@color/tusky_orange" + sparkbutton:secondaryColor="@color/tusky_orange_light" /> + + <at.connyduck.sparkbutton.SparkButton + android:id="@+id/status_bookmark" + android:layout_width="30dp" + android:layout_height="30dp" + android:clipToPadding="false" + android:contentDescription="@string/action_bookmark" + android:importantForAccessibility="no" + android:padding="4dp" + app:layout_constraintEnd_toStartOf="@id/status_more" + app:layout_constraintStart_toEndOf="@id/status_favourite" + app:layout_constraintTop_toTopOf="@id/status_reply" + sparkbutton:activeImage="@drawable/ic_bookmark_active_24dp" + sparkbutton:iconSize="28dp" + sparkbutton:inactiveImage="@drawable/ic_bookmark_24dp" + sparkbutton:primaryColor="@color/tusky_green" + sparkbutton:secondaryColor="@color/tusky_green_light" /> + + <ImageButton + android:id="@+id/status_more" + style="@style/TuskyImageButton" + android:layout_width="24dp" + android:layout_height="30dp" + android:layout_marginEnd="8dp" + android:contentDescription="@string/action_more" + android:padding="4dp" + app:layout_constraintBottom_toBottomOf="@id/status_reply" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/status_bookmark" + app:layout_constraintTop_toTopOf="@id/status_reply" + app:srcCompat="@drawable/ic_more_horiz_24dp" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_draft.xml b/app/src/main/res/layout/item_draft.xml new file mode 100644 index 0000000..23e2471 --- /dev/null +++ b/app/src/main/res/layout/item_draft.xml @@ -0,0 +1,95 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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_marginBottom="8dp" + android:background="?attr/selectableItemBackground" + android:paddingTop="4dp" + android:paddingBottom="4dp"> + + <TextView + android:id="@+id/draftSendingInfo" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="4dp" + android:layout_marginEnd="8dp" + android:drawablePadding="4dp" + android:fontFamily="sans-serif-medium" + android:gravity="center_vertical" + android:text="@string/drafts_post_failed_to_send" + android:textColor="@color/tusky_red" + android:textSize="?attr/status_text_medium" + app:drawableStartCompat="@drawable/ic_alert_circle" + app:drawableTint="@color/tusky_red" + app:layout_constraintEnd_toStartOf="@id/deleteButton" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/contentWarning" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="4dp" + android:layout_marginEnd="8dp" + android:fontFamily="sans-serif-medium" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toStartOf="@id/deleteButton" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/draftSendingInfo" + tools:text="Some content warning" /> + + <TextView + android:id="@+id/content" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="4dp" + android:layout_marginEnd="8dp" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toStartOf="@id/deleteButton" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/contentWarning" + tools:text="Some post content. May be very long." /> + + <ImageButton + android:id="@+id/deleteButton" + style="@style/TuskyImageButton" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_gravity="center_vertical" + android:layout_margin="12dp" + android:contentDescription="@string/action_delete" + android:padding="4dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0" + app:srcCompat="@drawable/ic_clear_24dp" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/draftMediaPreview" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginEnd="8dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/content" /> + + <com.keylesspalace.tusky.components.compose.view.PollPreviewView + android:id="@+id/draftPoll" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_marginTop="8dp" + android:minWidth="@dimen/poll_preview_min_width" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/draftMediaPreview" + app:layout_goneMarginEnd="8dp" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/item_edit_field.xml b/app/src/main/res/layout/item_edit_field.xml new file mode 100644 index 0000000..a658343 --- /dev/null +++ b/app/src/main/res/layout/item_edit_field.xml @@ -0,0 +1,57 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginEnd="16dp" + android:layout_marginBottom="8dp" + android:orientation="horizontal"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:colorBackground" + android:orientation="vertical" + android:paddingStart="16dp" + android:paddingTop="8dp" + android:paddingEnd="16dp" + android:paddingBottom="8dp"> + + <com.google.android.material.textfield.TextInputLayout + style="@style/TuskyTextInput" + android:id="@+id/accountFieldNameTextLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:hint="@string/profile_metadata_label_label" + app:counterTextColor="?android:textColorTertiary"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/accountFieldNameText" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:importantForAutofill="no" /> + + </com.google.android.material.textfield.TextInputLayout> + + <com.google.android.material.textfield.TextInputLayout + style="@style/TuskyTextInput" + android:id="@+id/accountFieldValueTextLayout" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:hint="@string/profile_metadata_content_label" + app:counterTextColor="?android:textColorTertiary"> + + <com.google.android.material.textfield.TextInputEditText + android:id="@+id/accountFieldValueText" + android:lineSpacingMultiplier="1.1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:importantForAutofill="no" /> + + </com.google.android.material.textfield.TextInputLayout> + + </LinearLayout> + +</androidx.cardview.widget.CardView> \ No newline at end of file diff --git a/app/src/main/res/layout/item_emoji_button.xml b/app/src/main/res/layout/item_emoji_button.xml new file mode 100644 index 0000000..618a957 --- /dev/null +++ b/app/src/main/res/layout/item_emoji_button.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<ImageView xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/composeEmojiButton" + android:background="?attr/selectableItemBackgroundBorderless" + android:layout_width="40dp" + android:layout_height="40dp" + android:layout_margin="4dp" + android:padding="4dp" + xmlns:android="http://schemas.android.com/apk/res/android" + tools:ignore="ContentDescription" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_follow.xml b/app/src/main/res/layout/item_follow.xml new file mode 100644 index 0000000..6dcbf60 --- /dev/null +++ b/app/src/main/res/layout/item_follow.xml @@ -0,0 +1,86 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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:orientation="vertical" + android:paddingLeft="14dp" + android:paddingRight="14dp" + android:paddingBottom="10dp"> + + <TextView + android:id="@+id/notification_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:drawablePadding="10dp" + android:ellipsize="end" + android:gravity="center_vertical" + android:maxLines="1" + android:paddingStart="28dp" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" + app:drawableStartCompat="@drawable/ic_person_add_24dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Someone followed you" /> + + <ImageView + android:id="@+id/notification_avatar" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_alignParentStart="false" + android:layout_marginTop="10dp" + android:contentDescription="@string/action_view_profile" + android:scaleType="centerCrop" + android:textSize="?attr/status_text_medium" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/notification_text" + tools:src="@drawable/avatar_default" /> + + <TextView + android:id="@+id/notification_display_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="6dp" + android:ellipsize="end" + android:maxLines="1" + android:paddingEnd="@dimen/status_display_name_padding_end" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + android:textStyle="normal|bold" + app:layout_constraintStart_toEndOf="@id/notification_avatar" + app:layout_constraintTop_toBottomOf="@+id/notification_text" + tools:text="Test User" + tools:ignore="RtlSymmetry" /> + + <TextView + android:id="@+id/notification_username" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:layout_constraintStart_toEndOf="@+id/notification_display_name" + app:layout_constraintTop_toTopOf="@+id/notification_display_name" + tools:text="\@testuser" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/notification_account_note" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="7dp" + android:hyphenationFrequency="full" + android:lineSpacingMultiplier="1.1" + android:textSize="?attr/status_text_medium" + android:textIsSelectable="true" + app:layout_constrainedWidth="true" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@+id/notification_display_name" + app:layout_constraintTop_toBottomOf="@+id/notification_display_name" + tools:text="Account note" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_follow_request.xml b/app/src/main/res/layout/item_follow_request.xml new file mode 100644 index 0000000..ce84c3c --- /dev/null +++ b/app/src/main/res/layout/item_follow_request.xml @@ -0,0 +1,121 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:paddingBottom="8dp"> + + <TextView + android:id="@+id/notificationTextView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:drawablePadding="10dp" + android:ellipsize="end" + android:gravity="center_vertical" + android:maxLines="1" + android:paddingStart="28dp" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:drawableStartCompat="@drawable/ic_person_add_24dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Someone requested to follow you" + tools:ignore="RtlSymmetry" /> + + <ImageView + android:id="@+id/avatar" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_centerVertical="false" + android:layout_marginTop="10dp" + android:contentDescription="@string/action_view_profile" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/notificationTextView" + tools:src="@drawable/avatar_default" /> + + <ImageView + android:id="@+id/avatarBadge" + android:layout_width="24dp" + android:layout_height="24dp" + android:contentDescription="@string/profile_badge_bot_text" + app:layout_constraintBottom_toBottomOf="@id/avatar" + app:layout_constraintEnd_toEndOf="@id/avatar" + app:srcCompat="@drawable/bot_badge" /> + + <TextView + android:id="@+id/displayNameTextView" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="6dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + android:textStyle="normal|bold" + app:layout_constraintEnd_toStartOf="@id/rejectButton" + app:layout_constraintStart_toEndOf="@+id/avatar" + app:layout_constraintTop_toBottomOf="@id/notificationTextView" + tools:text="Display name" /> + + <TextView + android:id="@+id/usernameTextView" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toStartOf="@+id/rejectButton" + app:layout_constraintStart_toEndOf="@id/avatar" + app:layout_constraintTop_toBottomOf="@id/displayNameTextView" + tools:text="\@username" /> + + <ImageButton + android:id="@+id/rejectButton" + style="@style/TuskyImageButton" + android:layout_width="52dp" + android:layout_height="48dp" + android:layout_marginStart="12dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/action_reject" + android:padding="4dp" + app:layout_constraintBottom_toBottomOf="@id/avatar" + app:layout_constraintEnd_toStartOf="@id/acceptButton" + app:layout_constraintStart_toEndOf="@id/displayNameTextView" + app:layout_constraintTop_toBottomOf="@+id/notificationTextView" + app:srcCompat="@drawable/ic_reject_24dp" /> + + <ImageButton + android:id="@+id/acceptButton" + style="@style/TuskyImageButton" + android:layout_width="52dp" + android:layout_height="48dp" + android:layout_marginEnd="4dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/action_accept" + android:padding="4dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@+id/rejectButton" + app:srcCompat="@drawable/ic_check_24dp" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/account_note" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:textSize="?attr/status_text_medium" + android:textIsSelectable="true" + android:hyphenationFrequency="full" + android:lineSpacingMultiplier="1.1" + app:layout_constraintEnd_toEndOf="@+id/acceptButton" + app:layout_constraintStart_toStartOf="@+id/usernameTextView" + app:layout_constraintTop_toBottomOf="@+id/rejectButton" + tools:text="Account note" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_follow_requests_header.xml b/app/src/main/res/layout/item_follow_requests_header.xml new file mode 100644 index 0000000..06e5c93 --- /dev/null +++ b/app/src/main/res/layout/item_follow_requests_header.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView 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:hyphenationFrequency="normal" + android:lineSpacingMultiplier="1.1" + android:paddingStart="16dp" + android:paddingTop="8dp" + android:paddingEnd="16dp" + android:paddingBottom="8dp" + tools:text="@string/follow_requests_info" /> diff --git a/app/src/main/res/layout/item_followed_hashtag.xml b/app/src/main/res/layout/item_followed_hashtag.xml new file mode 100644 index 0000000..6419af5 --- /dev/null +++ b/app/src/main/res/layout/item_followed_hashtag.xml @@ -0,0 +1,36 @@ +<?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:minHeight="?android:attr/listPreferredItemHeightSmall" + android:gravity="center_vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + > + + <TextView + android:id="@+id/followed_tag" + android:layout_width="0dp" + android:layout_weight="1" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:ellipsize="end" + android:maxLines="1" + android:textAppearance="?android:attr/textAppearanceListItemSmall" + tools:text="hashtag" /> + + <ImageButton + android:id="@+id/followed_tag_unfollow" + style="@style/TuskyImageButton" + android:layout_width="32dp" + android:layout_height="32dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/action_unfollow" + android:padding="4dp" + app:srcCompat="@drawable/ic_person_remove_24dp" + /> + +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/item_footer.xml b/app/src/main/res/layout/item_footer.xml new file mode 100644 index 0000000..bdf3bdd --- /dev/null +++ b/app/src/main/res/layout/item_footer.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="72dp"> + + <ProgressBar + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:indeterminate="true" /> + +</FrameLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/item_hashtag.xml b/app/src/main/res/layout/item_hashtag.xml new file mode 100644 index 0000000..8266eaf --- /dev/null +++ b/app/src/main/res/layout/item_hashtag.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- Copied from android.R.layout.simple_list_item_1, because view binding does not work with + android.R.layout.* --> + +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@android:id/text1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceListItemSmall" + android:gravity="center_vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:background="?attr/selectableItemBackground" + tools:ignore="SelectableText" /> diff --git a/app/src/main/res/layout/item_image_preview_overlay.xml b/app/src/main/res/layout/item_image_preview_overlay.xml new file mode 100644 index 0000000..88cbc2a --- /dev/null +++ b/app/src/main/res/layout/item_image_preview_overlay.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout + 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"> + + <com.keylesspalace.tusky.view.MediaPreviewImageView + android:id="@+id/preview_image_view" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:importantForAccessibility="no" + tools:ignore="RtlSymmetry" + tools:visibility="visible" /> + + <TextView + android:id="@+id/preview_media_description_indicator" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?android:attr/textColorSecondary" + android:textSize="?attr/status_text_small" + android:background="@drawable/media_warning_bg" + android:contentDescription="@null" + android:paddingLeft="5dp" + android:paddingTop="3dp" + android:paddingRight="5dp" + android:paddingBottom="3dp" + android:layout_margin="3dp" + android:visibility="gone" + android:text="@string/post_media_alt" + app:layout_constraintEnd_toEndOf="@+id/preview_image_view" + app:layout_constraintBottom_toBottomOf="@+id/preview_image_view" + app:tint="@color/white" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_list.xml b/app/src/main/res/layout/item_list.xml new file mode 100644 index 0000000..e1a99f9 --- /dev/null +++ b/app/src/main/res/layout/item_list.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingLeft="?dialogPreferredPadding" + android:paddingRight="?dialogPreferredPadding" + android:gravity="center_vertical" + android:orientation="horizontal" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:background="?attr/selectableItemBackground" + tools:ignore="Overdraw"> + + <TextView + android:id="@+id/list_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:drawablePadding="8dp" + android:gravity="center_vertical" + android:textAppearance="?android:attr/textAppearanceListItemSmall" + app:drawableStartCompat="@drawable/ic_list" + app:drawableTint="?android:attr/textColorSecondary" + tools:text="Example list" /> + + <ImageButton + android:id="@+id/more_button" + style="@style/TuskyImageButton" + android:layout_width="32dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:background="?selectableItemBackgroundBorderless" + android:contentDescription="@string/action_more" + android:visibility="gone" + app:srcCompat="@drawable/ic_more_horiz_24dp" /> + + <ImageButton + android:id="@+id/add_button" + style="@style/TuskyImageButton" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_marginStart="8dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/action_add_to_list" + android:visibility="gone" + app:srcCompat="@drawable/ic_plus_24dp" /> + + <ImageButton + android:id="@+id/remove_button" + style="@style/TuskyImageButton" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_marginStart="8dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/action_remove_from_list" + android:visibility="gone" + app:srcCompat="@drawable/ic_clear_24dp" /> +</LinearLayout> diff --git a/app/src/main/res/layout/item_media_preview.xml b/app/src/main/res/layout/item_media_preview.xml new file mode 100644 index 0000000..fda3473 --- /dev/null +++ b/app/src/main/res/layout/item_media_preview.xml @@ -0,0 +1,114 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge 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" + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> + + <com.keylesspalace.tusky.view.MediaPreviewLayout + android:id="@+id/status_media_preview" + android:layout_width="0dp" + android:layout_height="wrap_content" + app:layout_constraintEnd_toStartOf="@+id/status_media_preview_1" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <ImageView + android:id="@+id/status_sensitive_media_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="3dp" + android:background="@drawable/media_warning_bg" + android:contentDescription="@null" + android:padding="@dimen/status_sensitive_media_button_padding" + android:visibility="gone" + app:layout_constraintStart_toStartOf="@+id/status_media_preview_container" + app:layout_constraintTop_toTopOf="@+id/status_media_preview_container" + app:srcCompat="@drawable/ic_eye_24dp" + app:tint="?android:attr/textColorSecondary" /> + + <TextView + android:id="@+id/status_sensitive_media_warning" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/media_warning_bg" + android:gravity="center" + android:lineSpacingMultiplier="1.2" + android:orientation="vertical" + android:paddingLeft="12dp" + android:paddingTop="8dp" + android:paddingRight="12dp" + android:paddingBottom="8dp" + android:textAlignment="center" + android:textColor="?android:attr/textColorSecondary" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/status_media_label_0" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:drawablePadding="4dp" + android:ellipsize="end" + android:gravity="center_vertical" + android:importantForAccessibility="no" + android:maxLines="10" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:drawableTint="?android:attr/textColorTertiary" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/status_media_label_1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:drawablePadding="4dp" + android:ellipsize="end" + android:gravity="center_vertical" + android:importantForAccessibility="no" + android:maxLines="10" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:drawableTint="?android:attr/textColorTertiary" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_media_label_0" /> + + <TextView + android:id="@+id/status_media_label_2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:drawablePadding="4dp" + android:ellipsize="end" + android:gravity="center_vertical" + android:importantForAccessibility="no" + android:maxLines="10" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:drawableTint="?android:attr/textColorTertiary" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_media_label_1" /> + + <TextView + android:id="@+id/status_media_label_3" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:drawablePadding="4dp" + android:ellipsize="end" + android:gravity="center_vertical" + android:importantForAccessibility="no" + android:maxLines="10" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:drawableTint="?android:attr/textColorTertiary" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_media_label_2" /> + +</merge> diff --git a/app/src/main/res/layout/item_muted_user.xml b/app/src/main/res/layout/item_muted_user.xml new file mode 100644 index 0000000..463867a --- /dev/null +++ b/app/src/main/res/layout/item_muted_user.xml @@ -0,0 +1,89 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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:paddingStart="16dp" + android:paddingEnd="16dp"> + + <ImageView + android:id="@+id/muted_user_avatar" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_centerVertical="true" + android:layout_marginTop="10dp" + android:layout_marginBottom="10dp" + android:contentDescription="@string/action_view_profile" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/avatar_default" /> + + <ImageView + android:id="@+id/muted_user_bot_badge" + android:layout_width="24dp" + android:layout_height="24dp" + android:contentDescription="@string/profile_badge_bot_text" + app:layout_constraintBottom_toBottomOf="@id/muted_user_avatar" + app:layout_constraintEnd_toEndOf="@id/muted_user_avatar" + app:srcCompat="@drawable/bot_badge" /> + + <TextView + android:id="@+id/muted_user_display_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_large" + android:textStyle="normal|bold" + app:layout_constrainedWidth="true" + app:layout_constraintBottom_toTopOf="@id/muted_user_username" + app:layout_constraintEnd_toStartOf="@id/muted_user_unmute" + app:layout_constraintStart_toEndOf="@id/muted_user_avatar" + app:layout_constraintTop_toTopOf="@id/muted_user_avatar" + tools:text="Display name" /> + + <TextView + android:id="@+id/muted_user_username" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:layout_constraintBottom_toBottomOf="@id/muted_user_avatar" + app:layout_constraintEnd_toEndOf="@id/muted_user_display_name" + app:layout_constraintStart_toStartOf="@id/muted_user_display_name" + app:layout_constraintTop_toBottomOf="@id/muted_user_display_name" + tools:text="\@username" /> + + <ImageButton + android:id="@+id/muted_user_unmute" + style="@style/TuskyImageButton" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="12dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:padding="4dp" + app:layout_constraintBottom_toBottomOf="@id/muted_user_avatar" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/ic_clear_24dp" + tools:ignore="ContentDescription" /> + + <com.google.android.material.switchmaterial.SwitchMaterial + android:id="@+id/muted_user_mute_notifications" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:text="@string/mute_notifications_switch" + android:textColor="?android:textColorTertiary" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="@id/muted_user_display_name" + app:layout_constraintTop_toBottomOf="@id/muted_user_username" + app:switchPadding="4dp" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_network_state.xml b/app/src/main/res/layout/item_network_state.xml new file mode 100644 index 0000000..1674c30 --- /dev/null +++ b/app/src/main/res/layout/item_network_state.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:gravity="center" + android:padding="8dp"> + <TextView + android:id="@+id/errorMsg" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + <ProgressBar + android:id="@+id/progressBar" + style="?android:attr/progressBarStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + <Button + android:id="@+id/retryButton" + style="@style/TuskyButton.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/action_retry"/> +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/item_notifications_load_state_footer_view.xml b/app/src/main/res/layout/item_notifications_load_state_footer_view.xml new file mode 100644 index 0000000..4f80cd6 --- /dev/null +++ b/app/src/main/res/layout/item_notifications_load_state_footer_view.xml @@ -0,0 +1,45 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2023 Tusky Contributors + ~ + ~ This file is a part of Tusky. + ~ + ~ This program is free software; you can redistribute it and/or modify it under the terms of the + ~ GNU General Public License as published by the Free Software Foundation; either version 3 of the + ~ License, or (at your option) any later version. + ~ + ~ Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + ~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + ~ Public License for more details. + ~ + ~ You should have received a copy of the GNU General Public License along with Tusky; if not, + ~ see <http://www.gnu.org/licenses>. + --> + +<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" + android:padding="8dp"> + <androidx.core.widget.ContentLoadingProgressBar + android:id="@+id/progress_bar" + style="?android:attr/progressBarStyle" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + <TextView + android:id="@+id/error_msg" + android:textColor="?android:textColorPrimary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:textAlignment="center" + tools:text="@string/socket_timeout_exception"/> + <Button + android:id="@+id/retry_button" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:text="@string/action_retry"/> +</LinearLayout> diff --git a/app/src/main/res/layout/item_poll.xml b/app/src/main/res/layout/item_poll.xml new file mode 100644 index 0000000..fdc0614 --- /dev/null +++ b/app/src/main/res/layout/item_poll.xml @@ -0,0 +1,43 @@ +<?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" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <TextView + android:id="@+id/status_poll_option_result" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:background="@drawable/poll_option_background" + android:maxLines="3" + android:ellipsize="end" + android:paddingStart="6dp" + android:paddingTop="2dp" + android:paddingEnd="6dp" + android:paddingBottom="2dp" + android:textAlignment="viewStart" + android:textColor="?android:attr/textColorPrimary" + android:textSize="?attr/status_text_medium" + tools:text="40%" /> + + <RadioButton + android:id="@+id/status_poll_radio_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?android:attr/textColorPrimary" + android:textSize="?attr/status_text_medium" + app:buttonTint="@color/compound_button_color" + tools:text="Option 1" /> + + <CheckBox + android:id="@+id/status_poll_checkbox" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?android:attr/textColorPrimary" + android:textSize="?attr/status_text_medium" + app:buttonTint="@color/compound_button_color" + tools:text="Option 1" /> + +</FrameLayout> diff --git a/app/src/main/res/layout/item_poll_preview_option.xml b/app/src/main/res/layout/item_poll_preview_option.xml new file mode 100644 index 0000000..e8fa984 --- /dev/null +++ b/app/src/main/res/layout/item_poll_preview_option.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:drawablePadding="4dp" + android:ellipsize="end" + android:focusableInTouchMode="false" + android:gravity="center_vertical" + android:lines="1" + android:maxEms="20" + app:drawableTint="?android:attr/textColorTertiary" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_removable.xml b/app/src/main/res/layout/item_removable.xml new file mode 100644 index 0000000..803715d --- /dev/null +++ b/app/src/main/res/layout/item_removable.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <TextView + android:id="@+id/textPrimary" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="0.91" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toStartOf="@id/delete" + app:layout_constraintTop_toTopOf="parent" + android:paddingStart="8dp" + android:paddingEnd="8dp" + android:paddingTop="8dp" + android:textSize="?attr/status_text_medium" + android:textColor="@color/textColorPrimary" + /> + + <TextView + android:id="@+id/textSecondary" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_weight="0.91" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/textPrimary" + app:layout_constraintBottom_toBottomOf="parent" + android:paddingStart="8dp" + android:paddingEnd="8dp" + android:paddingBottom="8dp" + android:textSize="?attr/status_text_small" + android:textColor="@color/textColorTertiary" + /> + + <ImageButton + android:id="@+id/delete" + style="@style/TuskyImageButton" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_gravity="center_vertical" + android:layout_margin="12dp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/action_delete" + android:padding="4dp" + app:srcCompat="@drawable/ic_clear_24dp" /> + +</androidx.constraintlayout.widget.ConstraintLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/item_report_notification.xml b/app/src/main/res/layout/item_report_notification.xml new file mode 100644 index 0000000..fde968d --- /dev/null +++ b/app/src/main/res/layout/item_report_notification.xml @@ -0,0 +1,84 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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/notification_report" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingStart="14dp" + android:paddingEnd="14dp" + android:paddingBottom="14dp"> + + <TextView + android:id="@+id/notification_top_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:drawablePadding="10dp" + android:ellipsize="end" + android:gravity="center_vertical" + android:maxLines="1" + android:paddingStart="28dp" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:drawableStartCompat="@drawable/ic_flag_24dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Someone reported someone else" /> + + <ImageView + android:id="@+id/notification_reportee_avatar" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginTop="10dp" + android:layout_marginEnd="14dp" + android:contentDescription="@string/action_view_profile" + android:paddingEnd="12dp" + android:paddingBottom="12dp" + android:scaleType="centerCrop" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/notification_top_text" + tools:ignore="RtlHardcoded,RtlSymmetry" + tools:src="@drawable/avatar_default" /> + + <ImageView + android:id="@+id/notification_reporter_avatar" + android:layout_width="24dp" + android:layout_height="24dp" + android:contentDescription="@string/action_view_profile" + app:layout_constraintBottom_toBottomOf="@id/notification_reportee_avatar" + app:layout_constraintEnd_toEndOf="@id/notification_reportee_avatar" + tools:src="@drawable/avatar_default" /> + + <TextView + android:id="@+id/notification_summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="6dp" + android:hyphenationFrequency="full" + android:importantForAccessibility="no" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" + app:layout_constraintStart_toEndOf="@id/notification_reporter_avatar" + app:layout_constraintTop_toBottomOf="@id/notification_top_text" + tools:text="30 minutes ago · 2 posts attached" /> + + <TextView + android:id="@+id/notification_category" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:hyphenationFrequency="full" + android:importantForAccessibility="no" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" + android:textStyle="bold" + app:layout_constraintStart_toEndOf="@id/notification_reporter_avatar" + app:layout_constraintTop_toBottomOf="@id/notification_summary" + tools:text="Spam" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_report_status.xml b/app/src/main/res/layout/item_report_status.xml new file mode 100644 index 0000000..5331720 --- /dev/null +++ b/app/src/main/res/layout/item_report_status.xml @@ -0,0 +1,363 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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:paddingTop="8dp" + android:paddingBottom="8dp"> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/guideBegin" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:layout_constraintGuide_begin="8dp" /> + + <TextView + android:id="@+id/statusContentWarningDescription" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="@id/barrierEnd" + app:layout_constraintStart_toStartOf="@id/guideBegin" + app:layout_constraintTop_toTopOf="parent" + tools:text="content warning which is very long and it doesn't fit" + tools:visibility="visible" /> + + <Button + android:id="@+id/statusContentWarningButton" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:minWidth="150dp" + android:minHeight="0dp" + android:paddingLeft="16dp" + android:paddingTop="4dp" + android:paddingRight="16dp" + android:paddingBottom="4dp" + android:textAllCaps="true" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:layout_constraintStart_toStartOf="@id/guideBegin" + app:layout_constraintTop_toBottomOf="@id/statusContentWarningDescription" + tools:text="@string/post_content_warning_show_more" + tools:visibility="visible" /> + + <TextView + android:id="@+id/statusContent" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="8dp" + android:layout_weight="1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toStartOf="@id/barrierEnd" + app:layout_constraintStart_toStartOf="@id/guideBegin" + app:layout_constraintTop_toBottomOf="@id/statusContentWarningButton" /> + + <Button + android:id="@+id/buttonToggleContent" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:minWidth="150dp" + android:minHeight="0dp" + android:paddingLeft="16dp" + android:paddingTop="4dp" + android:paddingRight="16dp" + android:paddingBottom="4dp" + android:textAllCaps="true" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:layout_constraintStart_toStartOf="@id/guideBegin" + app:layout_constraintTop_toBottomOf="@id/statusContent" + tools:text="@string/post_content_show_less" + tools:visibility="visible" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/status_media_preview_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/status_media_preview_margin_top" + android:layout_marginEnd="8dp" + android:background="@drawable/media_preview_outline" + app:layout_constraintEnd_toStartOf="@id/barrierEnd" + app:layout_constraintStart_toStartOf="@id/guideBegin" + app:layout_constraintTop_toBottomOf="@id/buttonToggleContent" + tools:visibility="visible"> + + <com.keylesspalace.tusky.view.MediaPreviewImageView + android:id="@+id/status_media_preview_0" + android:layout_width="0dp" + android:layout_height="@dimen/status_media_preview_height" + android:scaleType="centerCrop" + app:layout_constraintEnd_toStartOf="@+id/status_media_preview_1" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:ignore="ContentDescription" /> + + <com.keylesspalace.tusky.view.MediaPreviewImageView + android:id="@+id/status_media_preview_1" + android:layout_width="0dp" + android:layout_height="@dimen/status_media_preview_height" + android:layout_marginStart="4dp" + android:scaleType="centerCrop" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/status_media_preview_0" + app:layout_constraintTop_toTopOf="parent" + tools:ignore="ContentDescription" /> + + + <com.keylesspalace.tusky.view.MediaPreviewImageView + android:id="@+id/status_media_preview_2" + android:layout_width="0dp" + android:layout_height="@dimen/status_media_preview_height" + android:layout_marginTop="4dp" + android:scaleType="centerCrop" + app:layout_constraintEnd_toStartOf="@+id/status_media_preview_3" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/status_media_preview_0" + tools:ignore="ContentDescription" /> + + <com.keylesspalace.tusky.view.MediaPreviewImageView + android:id="@+id/status_media_preview_3" + android:layout_width="0dp" + android:layout_height="@dimen/status_media_preview_height" + android:layout_marginStart="4dp" + android:layout_marginTop="4dp" + android:scaleType="centerCrop" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/status_media_preview_2" + app:layout_constraintTop_toBottomOf="@+id/status_media_preview_1" + tools:ignore="ContentDescription" /> + + <ImageView + android:id="@+id/status_media_overlay_0" + android:layout_width="0dp" + android:layout_height="0dp" + android:scaleType="center" + app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_0" + app:layout_constraintEnd_toEndOf="@+id/status_media_preview_0" + app:layout_constraintStart_toStartOf="@+id/status_media_preview_0" + app:layout_constraintTop_toTopOf="@+id/status_media_preview_0" + app:srcCompat="@drawable/ic_play_indicator" + tools:ignore="ContentDescription" /> + + <ImageView + android:id="@+id/status_media_overlay_1" + android:layout_width="0dp" + android:layout_height="0dp" + android:scaleType="center" + app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_1" + app:layout_constraintEnd_toEndOf="@+id/status_media_preview_1" + app:layout_constraintStart_toStartOf="@+id/status_media_preview_1" + app:layout_constraintTop_toTopOf="@+id/status_media_preview_1" + app:srcCompat="@drawable/ic_play_indicator" + tools:ignore="ContentDescription" /> + + <ImageView + android:id="@+id/status_media_overlay_2" + android:layout_width="0dp" + android:layout_height="0dp" + android:scaleType="center" + app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_2" + app:layout_constraintEnd_toEndOf="@+id/status_media_preview_2" + app:layout_constraintStart_toStartOf="@+id/status_media_preview_2" + app:layout_constraintTop_toTopOf="@+id/status_media_preview_2" + app:srcCompat="@drawable/ic_play_indicator" + tools:ignore="ContentDescription" /> + + <ImageView + android:id="@+id/status_media_overlay_3" + android:layout_width="0dp" + android:layout_height="0dp" + android:scaleType="center" + app:layout_constraintBottom_toBottomOf="@+id/status_media_preview_3" + app:layout_constraintEnd_toEndOf="@+id/status_media_preview_3" + app:layout_constraintStart_toStartOf="@+id/status_media_preview_3" + app:layout_constraintTop_toTopOf="@+id/status_media_preview_3" + app:srcCompat="@drawable/ic_play_indicator" + tools:ignore="ContentDescription" /> + + <ImageView + android:id="@+id/status_sensitive_media_button" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="3dp" + android:background="@drawable/media_warning_bg" + android:contentDescription="@null" + android:padding="@dimen/status_sensitive_media_button_padding" + android:visibility="gone" + app:layout_constraintLeft_toLeftOf="@+id/status_media_preview_container" + app:layout_constraintTop_toTopOf="@+id/status_media_preview_container" + app:srcCompat="@drawable/ic_eye_24dp" + app:tint="?android:attr/textColorSecondary" + tools:visibility="visible" /> + + <TextView + android:id="@+id/status_sensitive_media_warning" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="@drawable/media_warning_bg" + android:gravity="center" + android:lineSpacingMultiplier="1.2" + android:orientation="vertical" + android:paddingLeft="12dp" + android:paddingTop="8dp" + android:paddingRight="12dp" + android:paddingBottom="8dp" + android:textAlignment="center" + android:textColor="?android:attr/textColorSecondary" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <TextView + android:id="@+id/status_media_label" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?attr/selectableItemBackground" + android:drawablePadding="4dp" + android:gravity="center_vertical" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:drawableTint="?android:attr/textColorTertiary" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <TextView + android:id="@+id/status_poll_option_result_0" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:layout_marginEnd="8dp" + android:background="@drawable/poll_option_background" + android:ellipsize="end" + android:lines="1" + android:paddingStart="6dp" + android:paddingTop="2dp" + android:paddingEnd="6dp" + android:paddingBottom="2dp" + android:textColor="?android:attr/textColorPrimary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toStartOf="@id/barrierEnd" + app:layout_constraintStart_toStartOf="@id/guideBegin" + app:layout_constraintTop_toBottomOf="@id/status_media_preview_container" + tools:text="40%" /> + + <TextView + android:id="@+id/status_poll_option_result_1" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:layout_marginEnd="8dp" + android:background="@drawable/poll_option_background" + android:ellipsize="end" + android:lines="1" + android:paddingStart="6dp" + android:paddingTop="2dp" + android:paddingEnd="6dp" + android:paddingBottom="2dp" + android:textColor="?android:attr/textColorPrimary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toStartOf="@id/barrierEnd" + app:layout_constraintStart_toStartOf="@id/guideBegin" + app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_0" + tools:text="10%" /> + + <TextView + android:id="@+id/status_poll_option_result_2" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:layout_marginEnd="8dp" + android:background="@drawable/poll_option_background" + android:ellipsize="end" + android:lines="1" + android:paddingStart="6dp" + android:paddingTop="2dp" + android:paddingEnd="6dp" + android:paddingBottom="2dp" + android:textColor="?android:attr/textColorPrimary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toStartOf="@id/barrierEnd" + app:layout_constraintStart_toStartOf="@id/guideBegin" + app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_1" + tools:text="20%" /> + + <TextView + android:id="@+id/status_poll_option_result_3" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:layout_marginEnd="8dp" + android:background="@drawable/poll_option_background" + android:ellipsize="end" + android:lines="1" + android:paddingStart="6dp" + android:paddingTop="2dp" + android:paddingEnd="6dp" + android:paddingBottom="2dp" + android:textColor="?android:attr/textColorPrimary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toStartOf="@id/barrierEnd" + app:layout_constraintStart_toStartOf="@id/guideBegin" + app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_2" + tools:text="30%" /> + + <TextView + android:id="@+id/status_poll_description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:layout_marginEnd="8dp" + app:layout_constraintEnd_toEndOf="@id/barrierEnd" + app:layout_constraintStart_toStartOf="@id/guideBegin" + app:layout_constraintTop_toBottomOf="@id/status_poll_option_result_3" + tools:text="7 votes • 7 hours remaining" /> + + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/barrierEnd" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="start" + app:constraint_referenced_ids="statusSelection,timestampInfo" /> + + <TextView + android:id="@+id/timestampInfo" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="16dp" + android:importantForAccessibility="no" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/barrierEnd" + app:layout_constraintTop_toTopOf="parent" + tools:text="21 Dec 2018 18:45" /> + + <CheckBox + android:id="@+id/statusSelection" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center_vertical" + android:layout_margin="16dp" + app:buttonTint="@color/compound_button_color" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@id/timestampInfo" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_scheduled_status.xml b/app/src/main/res/layout/item_scheduled_status.xml new file mode 100644 index 0000000..4d9b5e4 --- /dev/null +++ b/app/src/main/res/layout/item_scheduled_status.xml @@ -0,0 +1,40 @@ +<?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" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <TextView + android:id="@+id/text" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="0.91" + android:padding="8dp" + android:textSize="?attr/status_text_medium" /> + + <ImageButton + android:id="@+id/edit" + style="@style/TuskyImageButton" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_gravity="center_vertical" + android:layout_margin="12dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/action_edit" + android:padding="4dp" + app:srcCompat="@drawable/ic_create_24dp" /> + + <ImageButton + android:id="@+id/delete" + style="@style/TuskyImageButton" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_gravity="center_vertical" + android:layout_margin="12dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/action_delete" + android:padding="4dp" + app:srcCompat="@drawable/ic_clear_24dp" /> + +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/item_status.xml b/app/src/main/res/layout/item_status.xml new file mode 100644 index 0000000..5f3d212 --- /dev/null +++ b/app/src/main/res/layout/item_status.xml @@ -0,0 +1,457 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:sparkbutton="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/status_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:clipToPadding="false" + android:focusable="true"> + + <TextView + android:id="@+id/status_info" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="@dimen/status_reblogged_bar_padding_top" + android:layout_marginEnd="14dp" + android:drawablePadding="6dp" + android:gravity="center_vertical" + android:importantForAccessibility="no" + android:paddingStart="38dp" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" + app:drawableStartCompat="@drawable/ic_reblog_18dp" + app:layout_constraintLeft_toRightOf="parent" + app:layout_constraintRight_toLeftOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:ignore="RtlSymmetry" + tools:text="ConnyDuck boosted" + tools:visibility="visible" /> + + <ImageView + android:id="@+id/status_avatar" + android:layout_width="@dimen/timeline_status_avatar_width" + android:layout_height="@dimen/timeline_status_avatar_height" + android:layout_marginStart="14dp" + android:layout_marginTop="@dimen/account_avatar_margin" + android:contentDescription="@string/action_view_profile" + android:importantForAccessibility="no" + android:scaleType="centerCrop" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_info" + tools:src="@drawable/avatar_default" /> + + <ImageView + android:id="@+id/status_avatar_inset" + android:layout_width="24dp" + android:layout_height="24dp" + android:contentDescription="@null" + android:importantForAccessibility="no" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="@id/status_avatar" + app:layout_constraintEnd_toEndOf="@id/status_avatar" + tools:src="#000" + tools:visibility="visible" /> + + <TextView + android:id="@+id/status_display_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="10dp" + android:ellipsize="end" + android:importantForAccessibility="no" + android:maxLines="1" + android:paddingEnd="@dimen/status_display_name_padding_end" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + android:textStyle="normal|bold" + app:layout_constrainedWidth="true" + app:layout_constraintEnd_toStartOf="@id/status_meta_info" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toEndOf="@id/status_avatar" + app:layout_constraintTop_toBottomOf="@id/status_info" + tools:text="Ente r the void you foooooo" /> + + <TextView + android:id="@+id/status_username" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:ellipsize="end" + android:importantForAccessibility="no" + android:maxLines="1" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toStartOf="@id/status_meta_info" + app:layout_constraintStart_toEndOf="@id/status_display_name" + app:layout_constraintTop_toTopOf="@id/status_display_name" + tools:text="\@Entenhausen@birbsarecooooooooooool.site" /> + + <TextView + android:id="@+id/status_meta_info" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="4dp" + android:layout_marginEnd="14dp" + android:importantForAccessibility="no" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@id/status_display_name" + tools:text="13:37" /> + + <TextView + android:id="@+id/status_translation_status" + style="@style/TextSizeSmall" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginEnd="10dp" + android:lineSpacingMultiplier="1.1" + android:maxLines="4" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_username" + tools:text="Translated from Lang by Service" + tools:visibility="visible" /> + + <Button + android:id="@+id/status_button_untranslate" + style="@style/TuskyButton.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingHorizontal="0dp" + android:layout_marginEnd="10dp" + android:minHeight="0dp" + android:text="@string/action_show_original" + android:visibility="gone" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_translation_status" + tools:visibility="visible" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/status_content_warning_description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="14dp" + android:hyphenationFrequency="full" + android:importantForAccessibility="no" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_button_untranslate" + tools:text="content warning which is very long and it doesn't fit" + tools:visibility="visible" /> + + <Button + android:id="@+id/status_content_warning_button" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:importantForAccessibility="no" + android:minWidth="150dp" + android:minHeight="0dp" + android:paddingLeft="16dp" + android:paddingTop="4dp" + android:paddingRight="16dp" + android:paddingBottom="4dp" + android:textAllCaps="true" + android:textOff="@string/post_content_warning_show_more" + android:textOn="@string/post_content_warning_show_less" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_content_warning_description" + tools:text="@string/post_content_warning_show_more" + tools:visibility="visible" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/status_content" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginEnd="14dp" + android:focusable="true" + android:hyphenationFrequency="full" + android:importantForAccessibility="no" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/status_content_warning_button" + app:layout_constraintTop_toBottomOf="@id/status_content_warning_button" + tools:text="This is a status" /> + + <LinearLayout + android:id="@+id/status_card_view" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginEnd="14dp" + android:background="@drawable/card_frame" + android:clipChildren="true" + android:foreground="?attr/selectableItemBackground" + android:minHeight="80dp" + android:orientation="vertical" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@+id/button_toggle_content" + tools:visibility="gone"> + + <com.google.android.material.imageview.ShapeableImageView + android:id="@+id/card_image" + android:layout_width="match_parent" + android:layout_height="300dp" + android:layout_margin="1dp" + android:background="?attr/colorBackgroundAccent" + android:importantForAccessibility="no" + android:scaleType="center" /> + + <LinearLayout + android:id="@+id/card_info" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingLeft="6dp" + android:paddingTop="6dp" + android:paddingRight="6dp" + android:paddingBottom="6dp"> + + <TextView + android:id="@+id/card_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:ellipsize="end" + android:fontFamily="sans-serif-medium" + android:lines="1" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" /> + + <TextView + android:id="@+id/card_description" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:ellipsize="end" + android:lineSpacingMultiplier="1.1" + android:maxLines="2" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" /> + + <TextView + android:id="@+id/card_link" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="end" + android:lines="1" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" /> + </LinearLayout> + </LinearLayout> + + <Button + android:id="@+id/button_toggle_content" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:importantForAccessibility="no" + android:minWidth="150dp" + android:minHeight="0dp" + android:paddingLeft="16dp" + android:paddingTop="4dp" + android:paddingRight="16dp" + android:paddingBottom="4dp" + android:textAllCaps="true" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_content" + tools:text="@string/post_content_show_less" + tools:visibility="visible" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/status_media_preview_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/status_media_preview_margin_top" + android:layout_marginEnd="14dp" + android:background="@drawable/media_preview_outline" + android:importantForAccessibility="noHideDescendants" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_card_view" + tools:visibility="visible"> + + <include layout="@layout/item_media_preview" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/status_poll_options" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginEnd="14dp" + android:layout_marginBottom="4dp" + android:nestedScrollingEnabled="false" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_media_preview_container" /> + + <Button + android:id="@+id/status_poll_button" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:gravity="center" + android:minWidth="150dp" + android:minHeight="0dp" + android:paddingLeft="16dp" + android:paddingTop="4dp" + android:paddingRight="16dp" + android:paddingBottom="4dp" + android:text="@string/poll_vote" + android:textSize="?attr/status_text_medium" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_poll_options" /> + + <TextView + android:id="@+id/status_poll_description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:layout_marginEnd="14dp" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_poll_button" + tools:text="7 votes • 7 hours remaining" /> + + <ImageButton + android:id="@+id/status_reply" + style="@style/TuskyImageButton" + android:layout_width="52dp" + android:layout_height="48dp" + android:layout_marginStart="-14dp" + android:contentDescription="@string/action_reply" + android:importantForAccessibility="no" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/status_inset" + app:layout_constraintHorizontal_chainStyle="spread_inside" + app:layout_constraintStart_toStartOf="@id/status_display_name" + app:layout_constraintTop_toBottomOf="@id/status_poll_description" + app:srcCompat="@drawable/ic_reply_24dp" + tools:ignore="NegativeMargin" /> + + <TextView + android:id="@+id/status_replies" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="45dp" + android:textSize="?attr/status_text_medium" + app:layout_constraintBottom_toBottomOf="@id/status_reply" + app:layout_constraintStart_toStartOf="@id/status_reply" + app:layout_constraintTop_toTopOf="@id/status_reply" + tools:text="1+" /> + + <at.connyduck.sparkbutton.SparkButton + android:id="@+id/status_inset" + android:layout_width="52dp" + android:layout_height="48dp" + android:clipToPadding="false" + android:contentDescription="@string/action_reblog" + android:importantForAccessibility="no" + android:padding="4dp" + app:layout_constraintEnd_toStartOf="@id/status_favourite" + app:layout_constraintStart_toEndOf="@id/status_reply" + app:layout_constraintTop_toTopOf="@id/status_reply" + sparkbutton:activeImage="@drawable/ic_reblog_active_24dp" + sparkbutton:iconSize="28dp" + sparkbutton:inactiveImage="@drawable/ic_reblog_24dp" + sparkbutton:primaryColor="@color/tusky_blue" + sparkbutton:secondaryColor="@color/tusky_blue_lighter" /> + + <TextView + android:id="@+id/status_insets" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="45dp" + android:textSize="?attr/status_text_medium" + app:layout_constraintBottom_toBottomOf="@id/status_inset" + app:layout_constraintStart_toStartOf="@id/status_inset" + app:layout_constraintTop_toTopOf="@id/status_inset" + tools:text="1+" /> + + <at.connyduck.sparkbutton.SparkButton + android:id="@+id/status_favourite" + android:layout_width="52dp" + android:layout_height="48dp" + android:clipToPadding="false" + android:contentDescription="@string/action_favourite" + android:importantForAccessibility="no" + android:padding="4dp" + app:layout_constraintEnd_toStartOf="@id/status_bookmark" + app:layout_constraintStart_toEndOf="@id/status_inset" + app:layout_constraintTop_toTopOf="@id/status_inset" + sparkbutton:activeImage="@drawable/ic_favourite_active_24dp" + sparkbutton:iconSize="28dp" + sparkbutton:inactiveImage="@drawable/ic_favourite_24dp" + sparkbutton:primaryColor="@color/tusky_orange" + sparkbutton:secondaryColor="@color/tusky_orange_light" /> + + <TextView + android:id="@+id/status_favourites_count" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="45dp" + android:textSize="?attr/status_text_medium" + app:layout_constraintBottom_toBottomOf="@id/status_inset" + app:layout_constraintStart_toStartOf="@id/status_favourite" + app:layout_constraintTop_toTopOf="@id/status_inset" + tools:text="" /> + + <at.connyduck.sparkbutton.SparkButton + android:id="@+id/status_bookmark" + android:layout_width="52dp" + android:layout_height="48dp" + android:clipToPadding="false" + android:contentDescription="@string/action_bookmark" + android:importantForAccessibility="no" + android:padding="4dp" + app:layout_constraintEnd_toStartOf="@id/status_more" + app:layout_constraintStart_toEndOf="@id/status_favourite" + app:layout_constraintTop_toTopOf="@id/status_reply" + sparkbutton:activeImage="@drawable/ic_bookmark_active_24dp" + sparkbutton:iconSize="28dp" + sparkbutton:inactiveImage="@drawable/ic_bookmark_24dp" + sparkbutton:primaryColor="@color/tusky_green" + sparkbutton:secondaryColor="@color/tusky_green_light" /> + + <ImageButton + android:id="@+id/status_more" + style="@style/TuskyImageButton" + android:layout_width="52dp" + android:layout_height="48dp" + android:layout_gravity="end" + android:contentDescription="@string/action_more" + android:importantForAccessibility="no" + app:layout_constraintBottom_toBottomOf="@id/status_reply" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/status_bookmark" + app:layout_constraintTop_toTopOf="@id/status_reply" + app:srcCompat="@drawable/ic_more_horiz_24dp" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_status_bottom_sheet.xml b/app/src/main/res/layout/item_status_bottom_sheet.xml new file mode 100644 index 0000000..23a6a06 --- /dev/null +++ b/app/src/main/res/layout/item_status_bottom_sheet.xml @@ -0,0 +1,20 @@ +<?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" + android:id="@+id/item_status_bottom_sheet" + android:layout_width="match_parent" + android:layout_height="80dp" + android:layout_gravity="bottom" + android:background="?android:colorBackground" + android:orientation="vertical" + app:behavior_hideable="true" + app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior"> + + <TextView + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="center" + android:text="@string/performing_lookup_title" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" /> +</LinearLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/item_status_detailed.xml b/app/src/main/res/layout/item_status_detailed.xml new file mode 100644 index 0000000..32b7c0a --- /dev/null +++ b/app/src/main/res/layout/item_status_detailed.xml @@ -0,0 +1,447 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:sparkbutton="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/status_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:clipToPadding="false"> + + <ImageView + android:id="@+id/status_avatar" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginStart="14dp" + android:layout_marginTop="14dp" + android:layout_marginEnd="14dp" + android:contentDescription="@string/action_view_profile" + android:importantForAccessibility="no" + android:scaleType="centerCrop" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:src="@drawable/avatar_default" /> + + <ImageView + android:id="@+id/status_avatar_inset" + android:layout_width="24dp" + android:layout_height="24dp" + android:contentDescription="@null" + android:importantForAccessibility="no" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="@id/status_avatar" + app:layout_constraintEnd_toEndOf="@id/status_avatar" + tools:src="#000" + tools:visibility="visible" /> + + <TextView + android:id="@+id/status_display_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="10dp" + android:layout_marginEnd="14dp" + android:ellipsize="end" + android:importantForAccessibility="no" + android:maxLines="1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + android:textStyle="normal|bold" + app:layout_constrainedWidth="true" + app:layout_constraintBottom_toTopOf="@id/status_username" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintStart_toEndOf="@id/status_avatar" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_chainStyle="packed" + tools:text="Display Name" /> + + <TextView + android:id="@+id/status_username" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="4dp" + android:layout_marginEnd="14dp" + android:ellipsize="end" + android:importantForAccessibility="no" + android:maxLines="1" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:layout_constrainedWidth="true" + app:layout_constraintBottom_toBottomOf="@id/status_avatar" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toEndOf="@id/status_avatar" + app:layout_constraintTop_toBottomOf="@id/status_display_name" + tools:text="\@ConnyDuck\@mastodon.social" /> + + <TextView + android:id="@+id/status_translation_status" + style="@style/TextSizeSmall" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="14dp" + android:lineSpacingMultiplier="1.1" + android:visibility="gone" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_avatar" + tools:text="Translated from blah using service" + tools:visibility="visible" /> + + <Button + android:id="@+id/status_button_untranslate" + style="@style/TuskyButton.TextButton" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginEnd="14dp" + android:minHeight="0dp" + android:paddingHorizontal="0dp" + android:text="@string/action_show_original" + android:visibility="gone" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_translation_status" + tools:visibility="visible" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/status_content_warning_description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="14dp" + android:hyphenationFrequency="full" + android:importantForAccessibility="no" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorPrimary" + android:textIsSelectable="true" + android:textSize="?attr/status_text_large" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_button_untranslate" + tools:text="CW this is a long long long long long long long long content warning" /> + + <com.google.android.material.button.MaterialButton + android:id="@+id/status_content_warning_button" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:importantForAccessibility="no" + android:minWidth="160dp" + android:minHeight="0dp" + android:paddingLeft="16dp" + android:paddingTop="4dp" + android:paddingRight="16dp" + android:paddingBottom="4dp" + android:textAllCaps="true" + android:textSize="?attr/status_text_large" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/status_content_warning_description" + tools:text="@string/post_content_warning_show_more" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/status_content" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="4dp" + android:layout_marginEnd="14dp" + android:layout_marginBottom="4dp" + android:focusable="true" + android:hyphenationFrequency="full" + android:importantForAccessibility="no" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorPrimary" + android:textIsSelectable="true" + android:textSize="?attr/status_text_large" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/status_content_warning_button" + tools:text="Status content. Can be pretty long. " /> + + <LinearLayout + android:id="@+id/status_card_view" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="8dp" + android:layout_marginEnd="14dp" + android:background="@drawable/card_frame" + android:clipChildren="true" + android:foreground="?attr/selectableItemBackground" + android:minHeight="80dp" + android:orientation="vertical" + app:layout_constraintTop_toBottomOf="@+id/status_content" + tools:visibility="gone"> + + <com.google.android.material.imageview.ShapeableImageView + android:id="@+id/card_image" + android:layout_width="match_parent" + android:layout_height="300dp" + android:layout_margin="1dp" + android:background="?attr/colorBackgroundAccent" + android:importantForAccessibility="no" + android:scaleType="center" /> + + <LinearLayout + android:id="@+id/card_info" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingLeft="6dp" + android:paddingTop="6dp" + android:paddingRight="6dp" + android:paddingBottom="6dp"> + + <TextView + android:id="@+id/card_title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:ellipsize="end" + android:fontFamily="sans-serif-medium" + android:lines="1" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" /> + + <TextView + android:id="@+id/card_description" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:ellipsize="end" + android:lineSpacingMultiplier="1.1" + android:maxLines="2" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" /> + + <TextView + android:id="@+id/card_link" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="end" + android:lines="1" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" /> + </LinearLayout> + + </LinearLayout> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/status_media_preview_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="12dp" + android:layout_marginEnd="14dp" + android:layout_marginBottom="4dp" + android:background="@drawable/media_preview_outline" + android:importantForAccessibility="noHideDescendants" + app:layout_constraintTop_toBottomOf="@id/status_card_view"> + + <include layout="@layout/item_media_preview" /> + + </androidx.constraintlayout.widget.ConstraintLayout> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/status_poll_options" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="4dp" + android:layout_marginEnd="14dp" + android:layout_marginBottom="4dp" + android:nestedScrollingEnabled="false" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_media_preview_container" /> + + <Button + android:id="@+id/status_poll_button" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="4dp" + android:gravity="center" + android:minWidth="150dp" + android:minHeight="0dp" + android:paddingLeft="16dp" + android:paddingTop="4dp" + android:paddingRight="16dp" + android:paddingBottom="4dp" + android:text="@string/poll_vote" + android:textSize="?attr/status_text_medium" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_poll_options" /> + + <TextView + android:id="@+id/status_poll_description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="6dp" + android:layout_marginEnd="14dp" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_poll_button" + tools:text="7 votes • 7 hours remaining" /> + + <TextView + android:id="@+id/status_meta_info" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="10dp" + android:layout_marginEnd="14dp" + android:drawablePadding="4dp" + android:importantForAccessibility="no" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_poll_description" + tools:text="21 Dec 2018 18:45" /> + + <View + android:id="@+id/status_info_divider" + android:layout_width="match_parent" + android:layout_height="0.5dp" + android:layout_below="@id/status_meta_info" + android:layout_marginStart="14dp" + android:layout_marginTop="6dp" + android:layout_marginEnd="14dp" + android:background="?android:attr/listDivider" + android:importantForAccessibility="no" + android:paddingStart="16dp" + android:paddingEnd="16dp" + app:layout_constraintTop_toBottomOf="@id/status_meta_info" /> + + <TextView + android:id="@+id/status_reblogs" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="6dp" + android:background="?attr/selectableItemBackground" + android:importantForAccessibility="no" + android:textSize="?attr/status_text_medium" + app:layout_constraintStart_toStartOf="@id/status_info_divider" + app:layout_constraintTop_toBottomOf="@id/status_info_divider" + tools:text="4 Boosts" + tools:visibility="visible" /> + + <TextView + android:id="@+id/status_favourites" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="12dp" + android:layout_marginTop="6dp" + android:background="?attr/selectableItemBackground" + android:importantForAccessibility="no" + android:textSize="?attr/status_text_medium" + app:layout_constraintStart_toEndOf="@id/status_reblogs" + app:layout_constraintTop_toBottomOf="@id/status_info_divider" + app:layout_goneMarginStart="0dp" + tools:text="8 Favorites" + tools:visibility="visible" /> + + <androidx.constraintlayout.widget.Barrier + android:id="@+id/status_counters_barrier" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + app:barrierDirection="bottom" + app:constraint_referenced_ids="status_reblogs,status_favourites" /> + + + <ImageButton + android:id="@+id/status_reply" + style="@style/TuskyImageButton" + android:layout_width="52dp" + android:layout_height="48dp" + android:layout_marginTop="4dp" + android:contentDescription="@string/action_reply" + android:importantForAccessibility="no" + android:padding="4dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/status_inset" + app:layout_constraintHorizontal_chainStyle="spread_inside" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_counters_barrier" + app:srcCompat="@drawable/ic_reply_24dp" /> + + <at.connyduck.sparkbutton.SparkButton + android:id="@+id/status_inset" + android:layout_width="52dp" + android:layout_height="48dp" + android:clipToPadding="false" + android:contentDescription="@string/action_reblog" + android:importantForAccessibility="no" + android:padding="4dp" + app:layout_constraintEnd_toStartOf="@id/status_favourite" + app:layout_constraintStart_toEndOf="@id/status_reply" + app:layout_constraintTop_toTopOf="@id/status_reply" + sparkbutton:activeImage="@drawable/ic_reblog_active_24dp" + sparkbutton:iconSize="28dp" + sparkbutton:inactiveImage="@drawable/ic_reblog_24dp" + sparkbutton:primaryColor="@color/tusky_blue" + sparkbutton:secondaryColor="@color/tusky_blue_lighter" /> + + <at.connyduck.sparkbutton.SparkButton + android:id="@+id/status_favourite" + android:layout_width="52dp" + android:layout_height="48dp" + android:clipToPadding="false" + android:contentDescription="@string/action_favourite" + android:importantForAccessibility="no" + android:padding="4dp" + app:layout_constraintEnd_toStartOf="@id/status_bookmark" + app:layout_constraintStart_toEndOf="@id/status_inset" + app:layout_constraintTop_toTopOf="@id/status_inset" + sparkbutton:activeImage="@drawable/ic_favourite_active_24dp" + sparkbutton:iconSize="28dp" + sparkbutton:inactiveImage="@drawable/ic_favourite_24dp" + sparkbutton:primaryColor="@color/tusky_orange" + sparkbutton:secondaryColor="@color/tusky_orange_light" /> + + <at.connyduck.sparkbutton.SparkButton + android:id="@+id/status_bookmark" + android:layout_width="52dp" + android:layout_height="48dp" + android:clipToPadding="false" + android:contentDescription="@string/action_bookmark" + android:importantForAccessibility="no" + android:padding="4dp" + app:layout_constraintEnd_toStartOf="@id/status_more" + app:layout_constraintStart_toEndOf="@id/status_favourite" + app:layout_constraintTop_toTopOf="@id/status_reply" + sparkbutton:activeImage="@drawable/ic_bookmark_active_24dp" + sparkbutton:iconSize="28dp" + sparkbutton:inactiveImage="@drawable/ic_bookmark_24dp" + sparkbutton:primaryColor="@color/tusky_green" + sparkbutton:secondaryColor="@color/tusky_green_light" /> + + <ImageButton + android:id="@+id/status_more" + style="@style/TuskyImageButton" + android:layout_width="52dp" + android:layout_height="48dp" + android:contentDescription="@string/action_more" + android:importantForAccessibility="no" + android:padding="4dp" + app:layout_constraintBottom_toBottomOf="@id/status_reply" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/status_bookmark" + app:layout_constraintTop_toTopOf="@id/status_reply" + app:srcCompat="@drawable/ic_more_horiz_24dp" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_status_edit.xml b/app/src/main/res/layout/item_status_edit.xml new file mode 100644 index 0000000..af0d2a1 --- /dev/null +++ b/app/src/main/res/layout/item_status_edit.xml @@ -0,0 +1,119 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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:gravity="center_vertical" + android:orientation="horizontal" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:paddingBottom="8dp"> + + <TextView + android:id="@+id/status_edit_info" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:ellipsize="end" + android:maxLines="2" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + android:textStyle="bold" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="\@Tusky edited 18th December 2022" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/status_edit_content_warning_description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:hyphenationFrequency="full" + android:importantForAccessibility="no" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="@+id/status_edit_info" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/status_edit_info" + tools:text="content warning which is very long and it doesn't fit" + tools:visibility="visible" /> + + <View + android:id="@+id/status_edit_content_warning_separator" + android:layout_width="0dp" + android:layout_height="1dp" + android:layout_marginTop="4dp" + android:background="?android:textColorPrimary" + android:importantForAccessibility="no" + app:layout_constraintEnd_toEndOf="@+id/status_edit_content_warning_description" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_edit_content_warning_description" /> + + <TextView + android:id="@+id/status_edit_content" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:focusable="true" + android:hyphenationFrequency="full" + android:importantForAccessibility="no" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorPrimary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toEndOf="@+id/status_edit_info" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_edit_content_warning_separator" + tools:text="This is an edited status" /> + + <com.keylesspalace.tusky.view.MediaPreviewLayout + android:id="@+id/status_edit_media_preview" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:background="@drawable/media_preview_outline" + android:importantForAccessibility="noHideDescendants" + app:layout_constraintEnd_toEndOf="@id/status_edit_info" + app:layout_constraintStart_toStartOf="@+id/status_edit_content_warning_description" + app:layout_constraintTop_toBottomOf="@id/status_edit_content" /> + + <TextView + android:id="@+id/status_edit_media_sensitivity" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:text="@string/post_sensitive_media_title" + android:textColor="?android:attr/textColorTertiary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toEndOf="@+id/status_edit_info" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_edit_media_preview" /> + + <!-- hidden because as of Mastodon 4.0.2 we don't get this info via the api --> + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/status_edit_poll_options" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:nestedScrollingEnabled="false" + app:layout_constraintEnd_toEndOf="@+id/status_edit_info" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_edit_media_sensitivity" /> + + <TextView + android:id="@+id/status_edit_poll_description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="6dp" + android:layout_marginEnd="14dp" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/status_edit_poll_options" + tools:text="ends at 12:30" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_status_filtered.xml b/app/src/main/res/layout/item_status_filtered.xml new file mode 100644 index 0000000..a091546 --- /dev/null +++ b/app/src/main/res/layout/item_status_filtered.xml @@ -0,0 +1,31 @@ +<?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/status_filtered_placeholder" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <TextView + android:id="@+id/status_filter_label" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginBottom="0dp" + android:text="Filter: MyFilter" + android:textAlignment="center" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + tools:ignore="HardcodedText" /> + + <Button + android:id="@+id/status_filter_show_anyway" + style="@style/TuskyButton.TextButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="0dp" + android:text="@string/status_filtered_show_anyway" + android:textSize="?attr/status_text_medium" + android:textStyle="bold" /> + +</LinearLayout> diff --git a/app/src/main/res/layout/item_status_notification.xml b/app/src/main/res/layout/item_status_notification.xml new file mode 100644 index 0000000..10aed2c --- /dev/null +++ b/app/src/main/res/layout/item_status_notification.xml @@ -0,0 +1,165 @@ +<?xml version="1.0" encoding="utf-8"?><!--This applies only to favourite and rebnotificationsEnabledions.--> +<androidx.constraintlayout.widget.ConstraintLayout 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/notification_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingLeft="14dp" + android:paddingRight="14dp" + android:paddingBottom="10dp"> + + <TextView + android:id="@+id/notification_top_text" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginBottom="6dp" + android:drawablePadding="10dp" + android:ellipsize="end" + android:gravity="center_vertical" + android:maxLines="1" + android:paddingStart="28dp" + android:textColor="?android:textColorSecondary" + android:textSize="?attr/status_text_medium" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="Someone favourited your status" /> + + <ImageView + android:id="@+id/notification_status_avatar" + android:layout_width="48dp" + android:layout_height="48dp" + android:layout_marginTop="10dp" + android:layout_marginBottom="14dp" + android:contentDescription="@string/action_view_profile" + android:scaleType="centerCrop" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/notification_top_text" + tools:ignore="RtlHardcoded,RtlSymmetry" + tools:src="@drawable/avatar_default" /> + + <ImageView + android:id="@+id/notification_notification_avatar" + android:layout_width="24dp" + android:layout_height="24dp" + android:contentDescription="@string/action_view_profile" + app:layout_constraintBottom_toBottomOf="@id/notification_status_avatar" + app:layout_constraintEnd_toEndOf="@id/notification_status_avatar" /> + + <TextView + android:id="@+id/status_display_name" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="6dp" + android:ellipsize="end" + android:maxLines="1" + android:paddingStart="0dp" + android:paddingEnd="@dimen/status_display_name_padding_end" + android:paddingBottom="4dp" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" + android:textStyle="normal|bold" + app:layout_constrainedWidth="true" + app:layout_constraintEnd_toStartOf="@id/status_meta_info" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintStart_toEndOf="@id/notification_notification_avatar" + app:layout_constraintTop_toBottomOf="@id/notification_top_text" + tools:text="Ente MM" /> + + <TextView + android:id="@+id/status_username" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:ellipsize="end" + android:maxLines="1" + android:paddingBottom="4dp" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toStartOf="@id/status_meta_info" + app:layout_constraintStart_toEndOf="@id/status_display_name" + app:layout_constraintTop_toTopOf="@+id/status_display_name" + tools:text="\@Entenhausen" /> + + <TextView + android:id="@+id/status_meta_info" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingBottom="4dp" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@+id/status_username" + tools:text="13:37" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/notification_content_warning_description" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:hyphenationFrequency="full" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/notification_status_avatar" + app:layout_constraintTop_toBottomOf="@id/status_display_name" + tools:text="Example CW text" /> + + <Button + android:id="@+id/notification_content_warning_button" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:minWidth="150dp" + android:minHeight="0dp" + android:paddingLeft="16dp" + android:paddingTop="4dp" + android:paddingRight="16dp" + android:paddingBottom="4dp" + android:textAllCaps="true" + android:textSize="?attr/status_text_medium" + app:layout_constraintStart_toEndOf="@id/notification_status_avatar" + app:layout_constraintTop_toBottomOf="@id/notification_content_warning_description" + tools:text="@string/post_content_warning_show_more" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/notification_content" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:hyphenationFrequency="full" + android:lineSpacingMultiplier="1.1" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/notification_status_avatar" + app:layout_constraintTop_toBottomOf="@id/notification_content_warning_button" + tools:text="Example status here" /> + + <Button + android:id="@+id/button_toggle_notification_content" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="14dp" + android:layout_marginTop="4dp" + android:minWidth="150dp" + android:minHeight="0dp" + android:paddingLeft="16dp" + android:paddingTop="4dp" + android:paddingRight="16dp" + android:paddingBottom="4dp" + android:textAllCaps="true" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:layout_constraintStart_toEndOf="@id/notification_status_avatar" + app:layout_constraintTop_toBottomOf="@id/notification_content" + tools:visibility="visible" + tools:text="@string/post_content_show_less" /> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_status_placeholder.xml b/app/src/main/res/layout/item_status_placeholder.xml new file mode 100644 index 0000000..4159e6b --- /dev/null +++ b/app/src/main/res/layout/item_status_placeholder.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="72dp" + android:background="@color/dividerColorOther"> + + <Button + android:id="@+id/loadMoreButton" + style="@style/TuskyButton.TextButton" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:insetTop="0dp" + android:insetBottom="0dp" + android:text="@string/load_more_placeholder_text" + android:textSize="?attr/status_text_large" + android:textStyle="bold" /> + + <ProgressBar + android:id="@+id/loadMoreProgressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" /> + +</FrameLayout> diff --git a/app/src/main/res/layout/item_status_wrapper.xml b/app/src/main/res/layout/item_status_wrapper.xml new file mode 100644 index 0000000..ad0df44 --- /dev/null +++ b/app/src/main/res/layout/item_status_wrapper.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <include layout="@layout/item_status" /> + + <include + layout="@layout/item_status_filtered" + android:visibility="gone" + /> +</FrameLayout> \ No newline at end of file diff --git a/app/src/main/res/layout/item_tab_preference.xml b/app/src/main/res/layout/item_tab_preference.xml new file mode 100644 index 0000000..145536e --- /dev/null +++ b/app/src/main/res/layout/item_tab_preference.xml @@ -0,0 +1,80 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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="?android:colorBackground" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:orientation="horizontal" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingTop="8dp" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd"> + + <ImageView + android:id="@+id/imageView" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end" + android:paddingTop="8dp" + android:paddingBottom="8dp" + app:layout_constraintBottom_toBottomOf="@id/textView" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/ic_drag_indicator_24dp" + tools:ignore="ContentDescription" /> + + <TextView + android:id="@+id/textView" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="8dp" + android:layout_weight="1" + android:drawablePadding="12dp" + android:paddingTop="8dp" + android:paddingBottom="8dp" + android:textAppearance="?android:attr/textAppearanceListItemSmall" + app:drawableTint="?android:attr/textColorSecondary" + app:layout_constraintBottom_toTopOf="@id/chipGroup" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/imageView" + app:layout_constraintTop_toTopOf="parent" + app:layout_goneMarginBottom="8dp" + tools:drawableStart="@drawable/ic_home_24dp" + tools:text="Home" /> + + <ImageButton + android:id="@+id/removeButton" + style="@style/TuskyImageButton" + android:layout_width="32dp" + android:layout_height="32dp" + android:layout_gravity="end" + android:layout_marginTop="4dp" + android:background="?attr/selectableItemBackgroundBorderless" + android:contentDescription="@string/action_delete" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/ic_clear_24dp" /> + + <com.google.android.material.chip.ChipGroup + android:id="@+id/chipGroup" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="8dp" + app:layout_constraintBottom_toBottomOf="parent"> + + <com.google.android.material.chip.Chip + android:id="@+id/actionChip" + style="@style/Widget.MaterialComponents.Chip.Action" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:checkable="false" + android:text="@string/add_hashtag_title" + android:textColor="?attr/colorOnPrimary" + app:chipIcon="@drawable/ic_plus_24dp" + app:chipIconTint="?attr/colorOnPrimary" + app:chipSurfaceColor="?attr/colorPrimary" /> + + </com.google.android.material.chip.ChipGroup> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_tab_preference_small.xml b/app/src/main/res/layout/item_tab_preference_small.xml new file mode 100644 index 0000000..2f46c05 --- /dev/null +++ b/app/src/main/res/layout/item_tab_preference_small.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/textView" + android:layout_width="match_parent" + android:layout_height="48dp" + android:background="?attr/selectableItemBackground" + android:drawablePadding="12dp" + android:ellipsize="end" + android:gravity="center_vertical" + android:lines="1" + android:paddingStart="8dp" + android:paddingEnd="8dp" + android:textAppearance="?android:attr/textAppearanceListItemSmall" + app:drawableStartCompat="@drawable/ic_home_24dp" + app:drawableTint="?android:attr/textColorSecondary" /> + + diff --git a/app/src/main/res/layout/item_trending_cell.xml b/app/src/main/res/layout/item_trending_cell.xml new file mode 100644 index 0000000..4328486 --- /dev/null +++ b/app/src/main/res/layout/item_trending_cell.xml @@ -0,0 +1,158 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout 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/trending_container" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:clipChildren="false" + android:clipToPadding="false" + android:focusable="true" + android:importantForAccessibility="yes" + android:padding="8dp" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + tools:layout_height="128dp"> + + <com.keylesspalace.tusky.view.GraphView + android:id="@+id/graph" + android:layout_width="0dp" + android:layout_height="120dp" + android:importantForAccessibility="no" + app:graphColor="?android:colorBackground" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/current_usage" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:lineWidth="2sp" + app:metaColor="?android:attr/textColorTertiary" + app:primaryLineColor="?attr/colorPrimary" + app:proportionalTrending="true" + app:secondaryLineColor="@color/warning_color" /> + + <TextView + android:id="@+id/current_usage" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + android:paddingStart="6dp" + android:textAlignment="textEnd" + android:textAppearance="@style/TextAppearance.AppCompat.Small" + android:textColor="?attr/colorPrimary" + android:textSize="8sp" + android:textStyle="normal" + app:layout_constrainedWidth="true" + app:layout_constraintBottom_toTopOf="@id/current_accounts" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/graph" + tools:text="12 345" /> + + <TextView + android:id="@+id/current_accounts" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:importantForAccessibility="no" + android:paddingStart="6dp" + android:textAlignment="textEnd" + android:textAppearance="@style/TextAppearance.AppCompat.Small" + android:textColor="@color/warning_color" + android:textSize="8sp" + android:textStyle="normal" + app:layout_constrainedWidth="true" + app:layout_constraintBottom_toBottomOf="@id/graph" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@id/graph" + tools:text="12 345" /> + + <androidx.constraintlayout.widget.ConstraintLayout + android:id="@+id/legend_container" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:background="?android:colorBackground" + android:backgroundTint="@color/color_background_transparent_60" + android:backgroundTintMode="src_in" + android:paddingTop="8dp" + android:paddingBottom="8dp" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + <TextView + android:id="@+id/tag" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:ellipsize="end" + android:importantForAccessibility="no" + android:singleLine="true" + android:textAlignment="textStart" + android:textAppearance="?android:attr/textAppearanceListItem" + android:textColor="?android:textColorPrimary" + android:textStyle="normal" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + tools:text="#itishashtagtuesdayitishashtagtuesday" /> + + <TextView + android:id="@+id/total_usage" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:importantForAccessibility="no" + android:textAlignment="textEnd" + android:textAppearance="?android:attr/textAppearanceListItemSmall" + android:textColor="?attr/colorPrimary" + android:textStyle="normal|bold" + app:layout_constrainedWidth="false" + app:layout_constraintEnd_toStartOf="@id/usageLabel" + app:layout_constraintHorizontal_chainStyle="spread_inside" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/tag" + tools:text="12 34" /> + + <TextView + android:id="@+id/usageLabel" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="4dp" + android:importantForAccessibility="no" + android:text="@string/total_usage" + android:textAlignment="textEnd" + android:textAppearance="?android:attr/textAppearanceListItemSecondary" + android:textColor="?android:textColorTertiary" + app:layout_constrainedWidth="false" + app:layout_constraintBaseline_toBaselineOf="@+id/total_usage" + app:layout_constraintEnd_toStartOf="@id/total_accounts" + app:layout_constraintStart_toEndOf="@+id/total_usage" /> + + <TextView + android:id="@+id/total_accounts" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginStart="16dp" + android:layout_marginTop="4dp" + android:importantForAccessibility="no" + android:textAlignment="textEnd" + android:textAppearance="?android:attr/textAppearanceListItemSmall" + android:textColor="@color/warning_color" + android:textStyle="normal|bold" + app:layout_constrainedWidth="false" + app:layout_constraintEnd_toStartOf="@+id/accountsLabel" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toEndOf="@id/usageLabel" + app:layout_constraintTop_toBottomOf="@+id/tag" + tools:text="498" /> + + <TextView + android:id="@+id/accountsLabel" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="4dp" + android:importantForAccessibility="no" + android:text="@string/total_accounts" + android:textAppearance="?android:attr/textAppearanceListItemSecondary" + android:textColor="?android:textColorTertiary" + app:layout_constrainedWidth="true" + app:layout_constraintBaseline_toBaselineOf="@+id/total_accounts" + app:layout_constraintStart_toEndOf="@id/total_accounts" /> + </androidx.constraintlayout.widget.ConstraintLayout> + +</androidx.constraintlayout.widget.ConstraintLayout> diff --git a/app/src/main/res/layout/item_trending_date.xml b/app/src/main/res/layout/item_trending_date.xml new file mode 100644 index 0000000..09481c7 --- /dev/null +++ b/app/src/main/res/layout/item_trending_date.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/dates" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?android:attr/listPreferredItemHeightSmall" + android:focusableInTouchMode="true" + android:importantForAccessibility="yes" + android:maxLines="1" + android:paddingTop="16dp" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:textAppearance="?android:attr/textAppearanceListItem" + android:textAlignment="textEnd" + android:textColor="?android:textColorTertiary" + tools:text="@string/date_range" /> diff --git a/app/src/main/res/layout/item_unknown_notification.xml b/app/src/main/res/layout/item_unknown_notification.xml new file mode 100644 index 0000000..09afceb --- /dev/null +++ b/app/src/main/res/layout/item_unknown_notification.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- paddingStart = 14dp+48dp+14dp = 76dp to align with avatars of other notifications --> +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingVertical="8dp" + android:lineSpacingMultiplier="1.1" + android:textSize="?attr/status_text_medium" + android:paddingStart="76dp" + android:paddingEnd="8dp" + android:text="@string/unknown_notification_type"/> diff --git a/app/src/main/res/layout/material_drawer_header.xml b/app/src/main/res/layout/material_drawer_header.xml new file mode 100644 index 0000000..683ab42 --- /dev/null +++ b/app/src/main/res/layout/material_drawer_header.xml @@ -0,0 +1,202 @@ +<!-- this replaces the default material_drawer_header.xml from the MaterialDrawer library to enable rounded avatars --> +<merge 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/material_drawer_account_header" + android:layout_width="match_parent" + android:layout_height="@dimen/material_drawer_account_header_height" + android:clickable="true" + android:focusable="true" + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/material_drawer_account_header_background" + android:layout_width="match_parent" + android:layout_height="@dimen/material_drawer_account_header_height" + android:scaleType="centerCrop" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/material_drawer_statusbar_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_begin="0dp" /> + + <com.keylesspalace.tusky.view.BezelImageView + android:id="@+id/material_drawer_account_header_current" + style="@style/BezelImageView" + android:layout_width="@dimen/material_drawer_account_header_selected" + android:layout_height="@dimen/material_drawer_account_header_selected" + android:layout_marginStart="@dimen/material_drawer_vertical_padding" + android:layout_marginTop="@dimen/material_drawer_account_header_horizontal_top" + android:clickable="true" + android:elevation="2dp" + android:focusable="true" + android:scaleType="fitCenter" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/material_drawer_statusbar_guideline" + app:materialDrawerMaskDrawable="@drawable/materialdrawer_shape_large" /> + + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/material_drawer_account_header_current_badge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:elevation="4dp" + android:fontFamily="sans-serif" + android:gravity="center" + android:lines="1" + android:minWidth="20dp" + android:paddingLeft="1dp" + android:paddingRight="1dp" + android:singleLine="true" + android:textSize="@dimen/material_drawer_item_badge_text" + app:layout_constraintBottom_toBottomOf="@id/material_drawer_account_header_current" + app:layout_constraintStart_toStartOf="@id/material_drawer_account_header_current" + tools:text="99" /> + + <com.keylesspalace.tusky.view.BezelImageView + android:id="@+id/material_drawer_account_header_small_first" + style="@style/BezelImageView" + android:layout_width="@dimen/material_drawer_account_header_secondary" + android:layout_height="@dimen/material_drawer_account_header_secondary" + android:layout_marginTop="@dimen/material_drawer_account_header_horizontal_top" + android:layout_marginEnd="@dimen/material_drawer_vertical_padding" + android:clickable="true" + android:elevation="2dp" + android:focusable="true" + android:scaleType="fitCenter" + android:visibility="visible" + app:layout_constraintEnd_toStartOf="@id/material_drawer_account_header_small_second" + app:layout_constraintTop_toBottomOf="@+id/material_drawer_statusbar_guideline" /> + + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/material_drawer_account_header_small_first_badge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:elevation="4dp" + android:fontFamily="sans-serif" + android:gravity="center" + android:lines="1" + android:minWidth="20dp" + android:paddingLeft="1dp" + android:paddingRight="1dp" + android:singleLine="true" + android:textSize="@dimen/material_drawer_item_badge_small_text" + app:layout_constraintBottom_toBottomOf="@id/material_drawer_account_header_small_first" + app:layout_constraintStart_toStartOf="@id/material_drawer_account_header_small_first" + tools:text="99" /> + + <com.keylesspalace.tusky.view.BezelImageView + android:id="@+id/material_drawer_account_header_small_second" + style="@style/BezelImageView" + android:layout_width="@dimen/material_drawer_account_header_secondary" + android:layout_height="@dimen/material_drawer_account_header_secondary" + android:layout_marginTop="@dimen/material_drawer_account_header_horizontal_top" + android:layout_marginEnd="@dimen/material_drawer_vertical_padding" + android:clickable="true" + android:elevation="2dp" + android:focusable="true" + android:scaleType="fitCenter" + android:visibility="visible" + app:layout_constraintEnd_toStartOf="@id/material_drawer_account_header_small_third" + app:layout_constraintTop_toBottomOf="@+id/material_drawer_statusbar_guideline" /> + + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/material_drawer_account_header_small_second_badge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:elevation="4dp" + android:fontFamily="sans-serif" + android:gravity="center" + android:lines="1" + android:minWidth="20dp" + android:paddingLeft="1dp" + android:paddingRight="1dp" + android:singleLine="true" + android:textSize="@dimen/material_drawer_item_badge_small_text" + app:layout_constraintBottom_toBottomOf="@id/material_drawer_account_header_small_second" + app:layout_constraintStart_toStartOf="@id/material_drawer_account_header_small_second" + tools:text="99" /> + + <com.keylesspalace.tusky.view.BezelImageView + android:id="@+id/material_drawer_account_header_small_third" + style="@style/BezelImageView" + android:layout_width="@dimen/material_drawer_account_header_secondary" + android:layout_height="@dimen/material_drawer_account_header_secondary" + android:layout_marginTop="@dimen/material_drawer_account_header_horizontal_top" + android:layout_marginEnd="@dimen/material_drawer_vertical_padding" + android:clickable="true" + android:elevation="2dp" + android:focusable="true" + android:scaleType="fitCenter" + android:visibility="visible" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toBottomOf="@+id/material_drawer_statusbar_guideline" /> + + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/material_drawer_account_header_small_third_badge" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:elevation="4dp" + android:fontFamily="sans-serif" + android:gravity="center" + android:lines="1" + android:minWidth="20dp" + android:paddingLeft="1dp" + android:paddingRight="1dp" + android:singleLine="true" + android:textSize="@dimen/material_drawer_item_badge_small_text" + app:layout_constraintBottom_toBottomOf="@id/material_drawer_account_header_small_third" + app:layout_constraintStart_toStartOf="@id/material_drawer_account_header_small_third" + tools:text="99" /> + + <androidx.constraintlayout.widget.Guideline + android:id="@+id/material_drawer_text_guideline" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:layout_constraintGuide_end="@dimen/material_drawer_account_header_dropdown_guideline" /> + + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/material_drawer_account_header_name" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/material_drawer_vertical_padding" + android:fontFamily="sans-serif-medium" + android:lines="1" + android:maxLines="1" + android:textSize="@dimen/material_drawer_account_header_title" + app:layout_constraintBottom_toTopOf="@id/material_drawer_account_header_email" + app:layout_constraintEnd_toStartOf="@id/material_drawer_account_header_text_switcher" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/material_drawer_text_guideline" + app:layout_constraintVertical_chainStyle="packed" /> + + <androidx.appcompat.widget.AppCompatTextView + android:id="@+id/material_drawer_account_header_email" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/material_drawer_vertical_padding" + android:layout_marginBottom="@dimen/material_drawer_padding" + android:fontFamily="sans-serif" + android:lines="1" + android:maxLines="1" + android:textSize="@dimen/material_drawer_account_header_subtext" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toStartOf="@id/material_drawer_account_header_text_switcher" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/material_drawer_account_header_name" /> + + <androidx.appcompat.widget.AppCompatImageView + android:id="@+id/material_drawer_account_header_text_switcher" + android:layout_width="@dimen/material_drawer_account_header_dropdown" + android:layout_height="@dimen/material_drawer_account_header_dropdown" + android:layout_marginEnd="@dimen/material_drawer_vertical_padding" + android:layout_marginBottom="@dimen/material_drawer_account_header_dropdown_margin_bottom" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" /> +</merge> diff --git a/app/src/main/res/layout/notifications_filter.xml b/app/src/main/res/layout/notifications_filter.xml new file mode 100644 index 0000000..20aa5f4 --- /dev/null +++ b/app/src/main/res/layout/notifications_filter.xml @@ -0,0 +1,19 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:orientation="vertical" android:layout_width="200dp" + android:layout_height="wrap_content" + android:background="?android:attr/windowBackground"> + <ListView + android:id="@+id/listView" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" /> + <Button + android:id="@+id/buttonApply" + style="@style/TuskyButton.TextButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_weight="0" + android:text="@string/filter_apply" + android:textSize="?attr/status_text_medium" /> +</LinearLayout> diff --git a/app/src/main/res/layout/pref_slider.xml b/app/src/main/res/layout/pref_slider.xml new file mode 100644 index 0000000..434a5e9 --- /dev/null +++ b/app/src/main/res/layout/pref_slider.xml @@ -0,0 +1,124 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2018 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<!-- Layout used by SeekBarPreference for the seekbar widget style. Minimally adapted for use + with Slider instead of SeekBar. --> +<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:minHeight="?android:attr/listPreferredItemHeightSmall" + android:gravity="center_vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:clipChildren="false" + android:clipToPadding="false" + android:baselineAligned="false"> + + <include layout="@layout/image_frame"/> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:layout_marginTop="8dp" + android:layout_marginBottom="8dp" + android:clipChildren="false" + android:clipToPadding="false"> + + <RelativeLayout + android:layout_width="wrap_content" + android:layout_height="0dp" + android:layout_weight="1"> + + <TextView + android:id="@android:id/title" + android:labelFor="@id/slider" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:singleLine="true" + android:textAppearance="?android:attr/textAppearanceListItem" + android:ellipsize="marquee" + tools:ignore="LabelFor,SelectableText" /> + + <TextView + android:id="@android:id/summary" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@android:id/title" + android:layout_alignStart="@android:id/title" + android:layout_gravity="start" + android:textAlignment="viewStart" + android:textColor="?android:attr/textColorSecondary" + android:maxLines="4" + style="@style/PreferenceSummaryTextStyle" + tools:ignore="SelectableText" /> + + </RelativeLayout> + + <!-- Using UnPressableLinearLayout as a workaround to disable the pressed state propagation + to the children of this container layout. Otherwise, the animated pressed state will also + play for the thumb in the AbsSeekBar in addition to the preference's ripple background. + The background of the SeekBar is also set to null to disable the ripple background --> + <androidx.preference.UnPressableLinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:paddingLeft="0dp" + android:paddingStart="0dp" + android:paddingRight="0dp" + android:paddingEnd="0dp" + android:clipChildren="false" + android:clipToPadding="false"> + + <com.google.android.material.button.MaterialButton + android:id="@+id/decrement" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + style="@style/Widget.Material3.Button.IconButton" /> + + <!-- The total height of the Seekbar widget's area should be 48dp - this allows for an + increased touch area so you do not need to exactly tap the thumb to move it. However, + setting the Seekbar height directly causes the thumb and seekbar to be misaligned on + API 22 and 23 - so instead we just set 15dp padding above and below, to account for the + 18dp default height of the Seekbar thumb for a total of 48dp. + Note: we set 0dp padding at the start and end of this seekbar to allow it to properly + fit into the layout, but this means that there's no leeway on either side for touch + input - this might be something we should reconsider down the line. --> + <com.google.android.material.slider.Slider + android:id="@+id/slider" + android:layout_width="0dp" + android:layout_weight="1" + android:layout_height="wrap_content" + android:paddingLeft="@dimen/preference_seekbar_padding_horizontal" + android:paddingStart="@dimen/preference_seekbar_padding_horizontal" + android:paddingRight="@dimen/preference_seekbar_padding_horizontal" + android:paddingEnd="@dimen/preference_seekbar_padding_horizontal" + android:paddingTop="@dimen/preference_seekbar_padding_vertical" + android:paddingBottom="@dimen/preference_seekbar_padding_vertical" + android:background="@null"/> + + <com.google.android.material.button.MaterialButton + android:id="@+id/increment" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + style="@style/Widget.Material3.Button.IconButton" /> + </androidx.preference.UnPressableLinearLayout> + </LinearLayout> +</LinearLayout> diff --git a/app/src/main/res/layout/search_view.xml b/app/src/main/res/layout/search_view.xml new file mode 100644 index 0000000..a856305 --- /dev/null +++ b/app/src/main/res/layout/search_view.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.appcompat.widget.SearchView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:searchHintIcon="@null" + android:maxWidth="10000dp" /> \ No newline at end of file diff --git a/app/src/main/res/layout/simple_list_item_1.xml b/app/src/main/res/layout/simple_list_item_1.xml new file mode 100644 index 0000000..239e9f1 --- /dev/null +++ b/app/src/main/res/layout/simple_list_item_1.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2006 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<!-- Copied from platform so it will work with view binding --> + +<TextView xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@android:id/text1" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textAppearance="?android:attr/textAppearanceListItemSmall" + android:gravity="center_vertical" + android:paddingStart="?android:attr/listPreferredItemPaddingStart" + android:paddingEnd="?android:attr/listPreferredItemPaddingEnd" + android:minHeight="?android:attr/listPreferredItemHeightSmall" /> diff --git a/app/src/main/res/layout/toolbar_basic.xml b/app/src/main/res/layout/toolbar_basic.xml new file mode 100644 index 0000000..47bd2d9 --- /dev/null +++ b/app/src/main/res/layout/toolbar_basic.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<com.google.android.material.appbar.AppBarLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/appbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:elevation="@dimen/actionbar_elevation" + app:layout_collapseMode="pin"> + + <androidx.appcompat.widget.Toolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" /> + +</com.google.android.material.appbar.AppBarLayout> diff --git a/app/src/main/res/layout/view_background_message.xml b/app/src/main/res/layout/view_background_message.xml new file mode 100644 index 0000000..12f0108 --- /dev/null +++ b/app/src/main/res/layout/view_background_message.xml @@ -0,0 +1,61 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical" + tools:parentTag="android.widget.LinearLayout"> + + <TextView + android:id="@+id/helpText" + android:visibility="gone" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:lineSpacingMultiplier="1.1" + android:textColor="@color/textColorPrimary" + android:background="@drawable/help_message_background" + android:layout_marginTop="16dp" + android:padding="16dp" + android:textAlignment="viewStart" + android:textSize="?attr/status_text_medium" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:gravity="center" + android:orientation="vertical"> + + <ImageView + android:id="@+id/imageView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:contentDescription="@null" + android:scaleType="centerInside" + tools:src="@drawable/errorphant_offline" /> + + <com.keylesspalace.tusky.view.ClickableSpanTextView + android:id="@+id/messageTextView" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:drawablePadding="16dp" + android:lineSpacingMultiplier="1.1" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:paddingTop="16dp" + android:text="@string/error_network" + android:textAlignment="center" + android:textSize="?attr/status_text_medium" /> + + <Button + android:id="@+id/button" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:layout_marginTop="8dp" + android:text="@string/action_retry" /> + </LinearLayout> +</merge> diff --git a/app/src/main/res/layout/view_compose_options.xml b/app/src/main/res/layout/view_compose_options.xml new file mode 100644 index 0000000..ee8c70e --- /dev/null +++ b/app/src/main/res/layout/view_compose_options.xml @@ -0,0 +1,62 @@ +<merge 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" + tools:layout_height="wrap_content" + tools:layout_width="match_parent" + tools:parentTag="RadioGroup"> + + <RadioButton + android:id="@+id/publicRadioButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginBottom="4dp" + android:layout_weight="1" + android:button="@drawable/ic_public_24dp" + android:paddingStart="10dp" + android:paddingEnd="0dp" + android:text="@string/visibility_public" + android:textColor="?android:textColorTertiary" + app:buttonTint="@color/compound_button_color" /> + + <RadioButton + android:id="@+id/unlistedRadioButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:layout_weight="1" + android:button="@drawable/ic_lock_open_24dp" + android:paddingStart="10dp" + android:paddingEnd="0dp" + android:text="@string/visibility_unlisted" + android:textColor="?android:textColorTertiary" + app:buttonTint="@color/compound_button_color" /> + + <RadioButton + android:id="@+id/privateRadioButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_marginBottom="4dp" + android:layout_weight="1" + android:button="@drawable/ic_lock_outline_24dp" + android:paddingStart="10dp" + android:paddingEnd="0dp" + android:text="@string/visibility_private" + android:textColor="?android:textColorTertiary" + app:buttonTint="@color/compound_button_color" /> + + <RadioButton + android:id="@+id/directRadioButton" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:layout_weight="1" + android:button="@drawable/ic_email_24dp" + android:paddingStart="10dp" + android:paddingEnd="0dp" + android:text="@string/visibility_direct" + android:textColor="?android:textColorTertiary" + app:buttonTint="@color/compound_button_color" /> + +</merge> \ No newline at end of file diff --git a/app/src/main/res/layout/view_compose_schedule.xml b/app/src/main/res/layout/view_compose_schedule.xml new file mode 100644 index 0000000..eeddfd2 --- /dev/null +++ b/app/src/main/res/layout/view_compose_schedule.xml @@ -0,0 +1,50 @@ +<merge 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" + tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout"> + + <Button + android:id="@+id/resetScheduleButton" + style="@style/TuskyButton.Outlined" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="16dp" + android:text="@string/action_reset_schedule" + app:layout_constraintBottom_toTopOf="@id/invalidScheduleWarning" + app:layout_constraintStart_toStartOf="parent" /> + + <TextView + android:id="@+id/scheduledDateTime" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:drawablePadding="4dp" + android:paddingStart="4dp" + android:paddingTop="4dp" + android:paddingBottom="16dp" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" + app:drawableTint="?attr/colorPrimary" + app:layout_constraintBottom_toTopOf="@id/invalidScheduleWarning" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="1" + app:layout_constraintStart_toEndOf="@id/resetScheduleButton" + tools:text="2020/01/01 00:00:00" /> + + <TextView + android:id="@+id/invalidScheduleWarning" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:drawablePadding="4dp" + android:paddingStart="4dp" + android:paddingTop="4dp" + android:paddingBottom="16dp" + android:textColor="?android:textColorTertiary" + android:textSize="?attr/status_text_medium" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHorizontal_bias="1" + app:layout_constraintStart_toStartOf="parent" + tools:text="@string/warning_scheduling_interval" /> + +</merge> diff --git a/app/src/main/res/layout/view_poll_preview.xml b/app/src/main/res/layout/view_poll_preview.xml new file mode 100644 index 0000000..87f7589 --- /dev/null +++ b/app/src/main/res/layout/view_poll_preview.xml @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge 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="wrap_content" + android:orientation="vertical" + tools:background="@drawable/card_frame" + tools:padding="@dimen/poll_preview_padding" + tools:parentTag="android.widget.LinearLayout"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:drawablePadding="4dp" + android:gravity="center_vertical" + android:text="@string/create_poll_title" + android:textStyle="bold" + app:drawableStartCompat="@drawable/ic_poll_24dp" /> + + <androidx.recyclerview.widget.RecyclerView + android:id="@+id/pollPreviewOptions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + android:nestedScrollingEnabled="false" + android:overScrollMode="never" + app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" /> + + <TextView + android:id="@+id/pollDurationPreview" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="4dp" + tools:text="5 Minutes" /> + +</merge> \ No newline at end of file diff --git a/app/src/main/res/menu/account_toolbar.xml b/app/src/main/res/menu/account_toolbar.xml new file mode 100644 index 0000000..b2f4db2 --- /dev/null +++ b/app/src/main/res/menu/account_toolbar.xml @@ -0,0 +1,54 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item android:id="@+id/action_open_in_web" + android:title="@string/action_open_in_web" + app:showAsAction="never" /> + + <item android:id="@+id/action_open_as" + android:title="@string/action_open_as" + app:showAsAction="never" /> + + <item + android:id="@+id/action_account_share" + android:title="@string/action_share"> + <menu> + <item + android:id="@+id/action_share_account_link" + android:title="@string/action_share_account_link" /> + <item + android:id="@+id/action_share_account_username" + android:title="@string/action_share_account_username" /> + </menu> + </item> + + <item android:id="@+id/action_mute" + android:title="@string/action_mute" + app:showAsAction="never" /> + + <item android:id="@+id/action_block" + android:title="@string/action_block" + app:showAsAction="never" /> + + <item android:id="@+id/action_add_or_remove_from_list" + android:title="@string/action_add_or_remove_from_list" + app:showAsAction="never" /> + + <item android:id="@+id/action_mute_domain" + android:title="@string/action_mute_domain" + app:showAsAction="never" /> + + <item android:id="@+id/action_show_reblogs" + android:title="@string/action_hide_reblogs" + app:showAsAction="never" /> + + <item + android:id="@+id/action_refresh" + android:title="@string/action_refresh" + app:showAsAction="never" /> + + <item + android:id="@+id/action_report" + android:title="@string/action_report" /> +</menu> diff --git a/app/src/main/res/menu/activity_announcements.xml b/app/src/main/res/menu/activity_announcements.xml new file mode 100644 index 0000000..bf72291 --- /dev/null +++ b/app/src/main/res/menu/activity_announcements.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/action_refresh" + android:title="@string/action_refresh" + app:showAsAction="never" /> +</menu> diff --git a/app/src/main/res/menu/activity_main.xml b/app/src/main/res/menu/activity_main.xml new file mode 100644 index 0000000..a1ca8b7 --- /dev/null +++ b/app/src/main/res/menu/activity_main.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/action_search" + android:title="@string/action_search" + app:showAsAction="ifRoom" /> +</menu> \ No newline at end of file diff --git a/app/src/main/res/menu/activity_scheduled_status.xml b/app/src/main/res/menu/activity_scheduled_status.xml new file mode 100644 index 0000000..bf72291 --- /dev/null +++ b/app/src/main/res/menu/activity_scheduled_status.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/action_refresh" + android:title="@string/action_refresh" + app:showAsAction="never" /> +</menu> diff --git a/app/src/main/res/menu/conversation_more.xml b/app/src/main/res/menu/conversation_more.xml new file mode 100644 index 0000000..2f5dedd --- /dev/null +++ b/app/src/main/res/menu/conversation_more.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/status_mute_conversation" + android:title="@string/action_mute_conversation" /> + <item + android:id="@+id/status_unmute_conversation" + android:title="@string/action_unmute_conversation" /> + <item + android:id="@+id/conversation_delete" + android:title="@string/action_delete_conversation" /> + +</menu> \ No newline at end of file diff --git a/app/src/main/res/menu/edit_profile_toolbar.xml b/app/src/main/res/menu/edit_profile_toolbar.xml new file mode 100644 index 0000000..49a9139 --- /dev/null +++ b/app/src/main/res/menu/edit_profile_toolbar.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/action_save" + android:icon="@drawable/ic_check_32dp" + android:title="@string/action_save" + app:showAsAction="always" /> +</menu> \ No newline at end of file diff --git a/app/src/main/res/menu/fragment_account_media.xml b/app/src/main/res/menu/fragment_account_media.xml new file mode 100644 index 0000000..bf72291 --- /dev/null +++ b/app/src/main/res/menu/fragment_account_media.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/action_refresh" + android:title="@string/action_refresh" + app:showAsAction="never" /> +</menu> diff --git a/app/src/main/res/menu/fragment_conversations.xml b/app/src/main/res/menu/fragment_conversations.xml new file mode 100644 index 0000000..bf72291 --- /dev/null +++ b/app/src/main/res/menu/fragment_conversations.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/action_refresh" + android:title="@string/action_refresh" + app:showAsAction="never" /> +</menu> diff --git a/app/src/main/res/menu/fragment_notifications.xml b/app/src/main/res/menu/fragment_notifications.xml new file mode 100644 index 0000000..876d408 --- /dev/null +++ b/app/src/main/res/menu/fragment_notifications.xml @@ -0,0 +1,35 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright 2023 Tusky Contributors + ~ + ~ This file is a part of Tusky. + ~ + ~ This program is free software; you can redistribute it and/or modify it under the terms of the + ~ GNU General Public License as published by the Free Software Foundation; either version 3 of the + ~ License, or (at your option) any later version. + ~ + ~ Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + ~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + ~ Public License for more details. + ~ + ~ You should have received a copy of the GNU General Public License along with Tusky; if not, + ~ see <http://www.gnu.org/licenses>. + --> + +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/action_refresh" + android:title="@string/action_refresh" + app:showAsAction="never" /> + + <item + android:id="@+id/action_edit_notification_filter" + android:title="@string/notifications_apply_filter" + app:showAsAction="never" /> + + <item + android:id="@+id/action_clear_notifications" + android:title="@string/notifications_clear" + app:showAsAction="never" /> +</menu> diff --git a/app/src/main/res/menu/fragment_report_statuses.xml b/app/src/main/res/menu/fragment_report_statuses.xml new file mode 100644 index 0000000..bf72291 --- /dev/null +++ b/app/src/main/res/menu/fragment_report_statuses.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/action_refresh" + android:title="@string/action_refresh" + app:showAsAction="never" /> +</menu> diff --git a/app/src/main/res/menu/fragment_search.xml b/app/src/main/res/menu/fragment_search.xml new file mode 100644 index 0000000..bf72291 --- /dev/null +++ b/app/src/main/res/menu/fragment_search.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/action_refresh" + android:title="@string/action_refresh" + app:showAsAction="never" /> +</menu> diff --git a/app/src/main/res/menu/fragment_timeline.xml b/app/src/main/res/menu/fragment_timeline.xml new file mode 100644 index 0000000..bf72291 --- /dev/null +++ b/app/src/main/res/menu/fragment_timeline.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/action_refresh" + android:title="@string/action_refresh" + app:showAsAction="never" /> +</menu> diff --git a/app/src/main/res/menu/fragment_view_edits.xml b/app/src/main/res/menu/fragment_view_edits.xml new file mode 100644 index 0000000..bf72291 --- /dev/null +++ b/app/src/main/res/menu/fragment_view_edits.xml @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/action_refresh" + android:title="@string/action_refresh" + app:showAsAction="never" /> +</menu> diff --git a/app/src/main/res/menu/fragment_view_thread.xml b/app/src/main/res/menu/fragment_view_thread.xml new file mode 100644 index 0000000..286ca56 --- /dev/null +++ b/app/src/main/res/menu/fragment_view_thread.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item android:id="@+id/action_open_in_web" + android:title="@string/action_open_in_web" + app:showAsAction="never" /> + + <item + android:id="@+id/action_reveal" + android:title="@string/expand_collapse_all_posts" + app:showAsAction="ifRoom" + android:icon="@drawable/ic_eye_24dp" /> + + <item + android:id="@+id/action_refresh" + android:title="@string/action_refresh" + app:showAsAction="never" /> +</menu> diff --git a/app/src/main/res/menu/list_actions.xml b/app/src/main/res/menu/list_actions.xml new file mode 100644 index 0000000..32bb836 --- /dev/null +++ b/app/src/main/res/menu/list_actions.xml @@ -0,0 +1,13 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/list_edit" + android:title="@string/action_add_or_remove_from_list" /> + <item + android:id="@+id/list_update" + android:title="@string/action_rename_list" /> + <item + android:id="@+id/list_delete" + android:title="@string/action_delete_list" /> + +</menu> \ No newline at end of file diff --git a/app/src/main/res/menu/search_toolbar.xml b/app/src/main/res/menu/search_toolbar.xml new file mode 100644 index 0000000..6ac1451 --- /dev/null +++ b/app/src/main/res/menu/search_toolbar.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/action_search" + android:title="@string/action_search" + android:icon="@android:drawable/ic_menu_search" + app:actionViewClass="androidx.appcompat.widget.SearchView" + android:actionLayout="@layout/search_view" + app:showAsAction="always" /> +</menu> diff --git a/app/src/main/res/menu/status_favourite.xml b/app/src/main/res/menu/status_favourite.xml new file mode 100644 index 0000000..66188c2 --- /dev/null +++ b/app/src/main/res/menu/status_favourite.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_action_favourite" + android:icon="@drawable/ic_favourite_24dp" + android:title="@string/action_favourite" /> + <item + android:id="@+id/menu_action_unfavourite" + android:icon="@drawable/ic_favourite_24dp" + android:title="@string/action_unfavourite" /> +</menu> diff --git a/app/src/main/res/menu/status_more.xml b/app/src/main/res/menu/status_more.xml new file mode 100644 index 0000000..1d85d67 --- /dev/null +++ b/app/src/main/res/menu/status_more.xml @@ -0,0 +1,42 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/status_share" + android:title="@string/action_share"> + <menu> + <item + android:id="@+id/post_share_link" + android:title="@string/post_share_link" /> + <item + android:id="@+id/post_share_content" + android:title="@string/post_share_content" /> + </menu> + </item> + <item + android:id="@+id/status_copy_link" + android:title="@string/action_copy_link" /> + <item + android:id="@+id/status_translate" + android:title="@string/action_translate" /> + <item + android:id="@+id/status_open_as" + android:title="@string/action_open_as" /> + <item + android:id="@+id/status_download_media" + android:title="@string/download_media" /> + <item + android:id="@+id/status_mute_conversation" + android:title="@string/action_mute_conversation" /> + + <group> + <item + android:id="@+id/status_mute" + android:title="@string/action_mute" /> + <item + android:id="@+id/status_block" + android:title="@string/action_block" /> + <item + android:id="@+id/status_report" + android:title="@string/action_report" /> + </group> +</menu> diff --git a/app/src/main/res/menu/status_more_for_user.xml b/app/src/main/res/menu/status_more_for_user.xml new file mode 100644 index 0000000..efb61f1 --- /dev/null +++ b/app/src/main/res/menu/status_more_for_user.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/status_share" + android:title="@string/action_share"> + <menu> + <item + android:id="@+id/post_share_link" + android:title="@string/post_share_link" /> + <item + android:id="@+id/post_share_content" + android:title="@string/post_share_content" /> + </menu> + </item> + <item + android:id="@+id/status_copy_link" + android:title="@string/action_copy_link" /> + <item + android:id="@+id/status_open_as" + android:title="@string/action_open_as" /> + <item + android:id="@+id/status_reblog_private" + android:title="@string/reblog_private" + android:visible="false" /> + <item + android:id="@+id/status_unreblog_private" + android:title="@string/unreblog_private" + android:visible="false" /> + <item + android:id="@+id/status_mute_conversation" + android:title="@string/action_mute_conversation" /> + <item + android:id="@+id/status_edit" + android:title="@string/action_edit" /> + <item + android:id="@+id/status_delete" + android:title="@string/action_delete" /> + <item + android:id="@+id/status_delete_and_redraft" + android:title="@string/action_delete_and_redraft" /> +</menu> diff --git a/app/src/main/res/menu/status_reblog.xml b/app/src/main/res/menu/status_reblog.xml new file mode 100644 index 0000000..4d79e6b --- /dev/null +++ b/app/src/main/res/menu/status_reblog.xml @@ -0,0 +1,11 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/menu_action_reblog" + android:icon="@drawable/ic_reblog_24dp" + android:title="@string/action_reblog" /> + <item + android:id="@+id/menu_action_unreblog" + android:icon="@drawable/ic_reblog_24dp" + android:title="@string/action_unreblog" /> +</menu> diff --git a/app/src/main/res/menu/view_hashtag_toolbar.xml b/app/src/main/res/menu/view_hashtag_toolbar.xml new file mode 100644 index 0000000..22d3774 --- /dev/null +++ b/app/src/main/res/menu/view_hashtag_toolbar.xml @@ -0,0 +1,34 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/action_follow_hashtag" + android:title="@string/action_follow" + app:showAsAction="ifRoom" + app:iconTint="?attr/colorOnSurface" + android:icon="@drawable/ic_person_add_24dp" /> + + <item + android:id="@+id/action_unfollow_hashtag" + android:title="@string/action_unfollow" + app:showAsAction="ifRoom" + app:iconTint="?attr/colorOnSurface" + android:icon="@drawable/ic_person_remove_24dp" /> + + <item + android:id="@+id/action_mute_hashtag" + android:title="@string/action_mute" + app:showAsAction="ifRoom" + app:iconTint="?attr/colorOnSurface" + android:icon="@drawable/ic_mute_24dp" /> + + <item + android:id="@+id/action_unmute_hashtag" + android:title="@string/action_unmute" + app:showAsAction="ifRoom" + app:iconTint="?attr/colorOnSurface" + android:icon="@drawable/ic_unmute_24dp" /> + +</menu> \ No newline at end of file diff --git a/app/src/main/res/menu/view_media_toolbar.xml b/app/src/main/res/menu/view_media_toolbar.xml new file mode 100644 index 0000000..e7a2227 --- /dev/null +++ b/app/src/main/res/menu/view_media_toolbar.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/action_download" + android:icon="@drawable/ic_file_download_black_24dp" + android:title="@string/dialog_download_image" + app:showAsAction="ifRoom" /> + <item + android:id="@+id/action_share_media" + android:icon="@drawable/ic_menu_share_24dp" + android:title="@string/action_share" + app:showAsAction="ifRoom" /> + <item + android:id="@+id/action_copy_media_link" + android:title="@string/action_copy_link" + app:showAsAction="never" /> + <item + android:id="@+id/action_open_status" + android:title="@string/action_open_post" + app:showAsAction="never" /> +</menu> \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..dd9dd02 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android" android:layout_height="108dp" android:layout_width="108dp"> + <background android:drawable="@color/icon_background"/> + <foreground android:drawable="@drawable/ic_launcher_foreground" /> + <monochrome android:drawable="@drawable/ic_launcher_monochrome"/> +</adaptive-icon> \ No newline at end of file diff --git a/app/src/main/res/raw/apache.txt b/app/src/main/res/raw/apache.txt new file mode 100644 index 0000000..c907272 --- /dev/null +++ b/app/src/main/res/raw/apache.txt @@ -0,0 +1,51 @@ +Apache License + +Version 2.0, January 2004 + +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + +a. You must give any other recipients of the Work or Derivative Works a copy of this License; and +b. You must cause any modified files to carry prominent notices stating that You changed the files; and +c. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and +d. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + +You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. \ No newline at end of file diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000..df784da --- /dev/null +++ b/app/src/main/res/values-ar/strings.xml @@ -0,0 +1,696 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">وقع هناك خطأ.</string> + <string name="error_network">حدث خطأ في الشبكة. يرجى التحقق من اتصالك ثم أعد المحاولة!</string> + <string name="error_empty">لا يجب أن يترك هذا الحقل فارغا.</string> + <string name="error_invalid_domain">اسم النطاق الذي قمتَ بإدخاله غير صالح</string> + <string name="error_failed_app_registration">أخففت المصادقة مع مثيل الخادم هذا. إذا استمرت هذه المشكلة، جرب تسجيل الدخول عبر المتصفح من القائمة.</string> + <string name="error_no_web_browser_found">تعذر العثور على متصفح ويب صالح للإستعمال.</string> + <string name="error_authorization_unknown">لقد وقع هناك خطأ مجهول في التصريح. إن استمرّ ذلك، يُرجى إعادة محاولة التسجيل بمساعدة متصفح الويب وذلك عبر القائمة.</string> + <string name="error_authorization_denied">تم رفض التصريح.</string> + <string name="error_retrieving_oauth_token">فشل الحصول على رمز الولوج.</string> + <string name="error_compose_character_limit">إنّ المنشور طويل جدا!</string> + <string name="error_media_upload_type">لا يمكن تحميل هذا النوع من الملفات.</string> + <string name="error_media_upload_opening">تعذر فتح ذاك الملف.</string> + <string name="error_media_upload_permission">التصريح لازم لقراءة الوسائط.</string> + <string name="error_media_download_permission">التصريح لازم للإحتفاظ بالوسائط.</string> + <string name="error_media_upload_image_or_video">لا يمكنك إرفاق كلا من الصور والفيديوهات في نفس المنشور في آن واحد.</string> + <string name="error_media_upload_sending">اخفقت عملية الرفع.</string> + <string name="error_sender_account_gone">خطأ عند إرسال المنشور.</string> + <string name="title_home">الرئيسي</string> + <string name="title_notifications">الاشعارات</string> + <string name="title_public_local">المحلي</string> + <string name="title_public_federated">الفدرالي</string> + <string name="title_direct_messages">الرسائل المباشرة</string> + <string name="title_tab_preferences">الألسنة</string> + <string name="title_view_thread">خيط</string> + <string name="title_posts">المنشورات</string> + <string name="title_posts_with_replies">بالردود</string> + <string name="title_posts_pinned">المدبّسة</string> + <string name="title_follows">المتابَعون</string> + <string name="title_followers">المتابِعون</string> + <string name="title_favourites">المفضلة</string> + <string name="title_mutes">الحسابات المكتومة</string> + <string name="title_blocks">الحسابات المحظورة</string> + <string name="title_follow_requests">طلبات المتابعة</string> + <string name="title_edit_profile">عدل ملفك التعريفي</string> + <string name="title_drafts">المسودات</string> + <string name="title_licenses">الرّخص</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">شاركه %1$s</string> + <string name="post_sensitive_media_title">محتوى حساس</string> + <string name="post_media_hidden_title">وسائط مخفية</string> + <string name="post_sensitive_media_directions">اضغط للعرض</string> + <string name="post_content_warning_show_more">اعرض المزيد</string> + <string name="post_content_warning_show_less">اعرض أقل</string> + <string name="post_content_show_more">توسيع</string> + <string name="post_content_show_less">تصغير</string> + <string name="message_empty">لا شيء هنا.</string> + <string name="footer_empty">لا يوجد شيء هنا. إسحب إلى أسفل للإنعاش!</string> + <string name="notification_reblog_format">شارك %1$s منشورك</string> + <string name="notification_favourite_format">أعجِب %1$s بمنشورك</string> + <string name="notification_follow_format">%1$s يتبعك</string> + <string name="report_username_format">أبلغ عن @%1$s</string> + <string name="report_comment_hint">تعليقات إضافية؟</string> + <string name="action_quick_reply">رد سريع</string> + <string name="action_reply">رد</string> + <string name="action_reblog">شارك</string> + <string name="action_unreblog">إلغاء إعادة المشاركة</string> + <string name="action_favourite">تفضيل</string> + <string name="action_unfavourite">إزالة المفضلة</string> + <string name="action_more">المزيد</string> + <string name="action_compose">حرر</string> + <string name="action_login">الولوج باستخدام Tusky</string> + <string name="action_logout">خروج</string> + <string name="action_logout_confirm">متأكد مِن أنك تود الخروج من الحساب %1$s؟</string> + <string name="action_follow">إتبع</string> + <string name="action_unfollow">إلغاء المتابعة</string> + <string name="action_block">قم بحظره</string> + <string name="action_unblock">إلغاء الحظر</string> + <string name="action_hide_reblogs">إخفاء المشاركات</string> + <string name="action_show_reblogs">إظهار المشاركات</string> + <string name="action_report">أبلغ عنه</string> + <string name="action_delete">إحذف</string> + <string name="action_send">بَوّق</string> + <string name="action_send_public">بوّق!</string> + <string name="action_retry">أعد المحاولة</string> + <string name="action_close">إغلاق</string> + <string name="action_view_profile">الملف التعريفي</string> + <string name="action_view_preferences">التفضيلات</string> + <string name="action_view_account_preferences">اعدادات الحساب</string> + <string name="action_view_favourites">المفضلة</string> + <string name="action_view_mutes">المستخدمون المكتومون</string> + <string name="action_view_blocks">المستخدمون المحظورون</string> + <string name="action_view_follow_requests">طلبات المتابعة</string> + <string name="action_view_media">وسائط</string> + <string name="action_open_in_web">إفتح في متصفح</string> + <string name="action_add_media">إضافة وسائط</string> + <string name="action_photo_take">أخذ صورة</string> + <string name="action_share">شارك</string> + <string name="action_mute">أكتم</string> + <string name="action_unmute">إلغاء الكتم</string> + <string name="action_mention">أذكر</string> + <string name="action_hide_media">إخفاء الوسائط</string> + <string name="action_open_drawer">إفتح الدرج</string> + <string name="action_save">إحفظ</string> + <string name="action_edit_profile">تعديل الملف التعريفي</string> + <string name="action_edit_own_profile">تعديل</string> + <string name="action_undo">إلغاء</string> + <string name="action_accept">موافقة</string> + <string name="action_reject">رفض</string> + <string name="action_search">البحث</string> + <string name="action_access_drafts">المسودات</string> + <string name="action_toggle_visibility">كيفية عرض المنشور</string> + <string name="action_content_warning">تحذير عن المحتوى</string> + <string name="action_emoji_keyboard">لوحة مفاتيح الإيموجي</string> + <string name="action_add_tab">إضافة لسان</string> + <string name="action_links">الروابط</string> + <string name="action_mentions">الإشارات</string> + <string name="action_hashtags">الوسوم</string> + <string name="action_open_reblogged_by">اعرض المشاركات</string> + <string name="action_open_faved_by">اعرض المفضلات</string> + <string name="title_hashtags_dialog">الوسوم</string> + <string name="title_mentions_dialog">الإشارات</string> + <string name="title_links_dialog">الروابط</string> + <string name="download_image">تنزيل %1$s</string> + <string name="action_copy_link">إنسخ الرابط</string> + <string name="action_open_as">إفتحه كـ %1$s</string> + <string name="action_share_as">شاركه كـ…</string> + <string name="send_post_link_to">شارك رابط التبويق مع…</string> + <string name="send_post_content_to">شارك التبويق على…</string> + <string name="send_media_to">شارك الوسيط مع…</string> + <string name="confirmation_reported">تم إرساله!</string> + <string name="confirmation_unblocked">تم فك الحجب عن الحساب</string> + <string name="confirmation_unmuted">لم يعد الحساب مكتومًا</string> + <string name="hint_domain">أي مثيل خادم؟</string> + <string name="hint_compose">ما الجديد؟</string> + <string name="hint_content_warning">تحذير عن المحتوى</string> + <string name="hint_display_name">الإسم العلني</string> + <string name="hint_note">السيرة</string> + <string name="hint_search">البحث عن…</string> + <string name="search_no_results">لم يتم العثور على أية نتائج</string> + <string name="label_quick_reply">رد…</string> + <string name="label_avatar">صورة الملف التعريفي</string> + <string name="label_header">صورة رأس الصفحة</string> + <string name="link_whats_an_instance">ماذا نعني بمثيل الخادم؟</string> + <string name="login_connection">الإتصال جارٍ…</string> + <string name="dialog_whats_an_instance">بإمكانك إدخال عنوان أي مثيل خادوم ماستدون هنا. على سبيل المثال mastodon.social أو icosahedron.website أو social.tchncs.de أوالإطلاع على <a href="https://instances.social">لاكتشاف المزيد !</a> +\n +\n إن كنت لا تملك حسابا بإمكانك إدخال اسم مثيل خادوم تريد الانضمام إليه قصد إنشاء حسابك عليه. +\n +\n نعني بمثيل الخادوم المكان الذي استُضِيف فيه حسابك و يمكنك التواصل مع أصدقائك و متابعيك و كأنكم على موقع واحد و ذلك حتى و إن كانت حساباتهم مُستضافة على مثيلات خوادم أخرى. +\n +\n للمزيد مِن التفاصيل إطّلع على <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="dialog_title_finishing_media_upload">تتمة رفع الوسائط</string> + <string name="dialog_message_uploading_media">الإرسال جارٍ…</string> + <string name="dialog_download_image">تنزيل</string> + <string name="dialog_message_cancel_follow_request">هل تريد رفض طلب المتابعة؟</string> + <string name="dialog_unfollow_warning">هل تود إلغاء متابعة هذا الحساب؟</string> + <string name="dialog_delete_post_warning">هل تريد حذف هذا التبويق؟</string> + <string name="visibility_public">للعامة: ينشر على الخيوط العمومية</string> + <string name="visibility_unlisted">غير مدرج: لا يُعرَض على الخيوط العمومية</string> + <string name="visibility_private">لمتابعيك فقط: يُنشر إلى متابعيك فقط</string> + <string name="visibility_direct">مباشر: يُنشر إلى المستخدمين المشار إليهم فقط</string> + <string name="pref_title_edit_notification_settings">تعديل الاشعارات</string> + <string name="pref_title_notifications_enabled">الإخطارات</string> + <string name="pref_title_notification_alerts">التنبيهات</string> + <string name="pref_title_notification_alert_sound">إعلام بالصوت</string> + <string name="pref_title_notification_alert_vibrate">إعلام بالاهتزار</string> + <string name="pref_title_notification_alert_light">إعلام بالضوء</string> + <string name="pref_title_notification_filters">أخطرني عندما</string> + <string name="pref_title_notification_filter_mentions">يشار إلي</string> + <string name="pref_title_notification_filter_follows">يتبعني أحدهم</string> + <string name="pref_title_notification_filter_reblogs">أُعيدَ نشر منشوراتي</string> + <string name="pref_title_notification_filter_favourites">يُعجَب أحد ما بمنشوراتي</string> + <string name="pref_title_appearance_settings">المظهر</string> + <string name="pref_title_app_theme">مظهر التطبيق</string> + <string name="pref_title_timelines">الخيوط الزمنية</string> + <string name="pref_title_timeline_filters">عوامل التصفية</string> + <string name="app_them_dark">داكنة</string> + <string name="app_theme_light">فاتحة</string> + <string name="app_theme_black">سوداء</string> + <string name="app_theme_auto">تلقائي عند غروب الشمس</string> + <string name="app_theme_system">استخدم مظهر النظام</string> + <string name="pref_title_browser_settings">المتصفح</string> + <string name="pref_title_custom_tabs">إخفاء زر المتابعة أثناء تمرير الصفحة</string> + <string name="pref_title_language">اللغة</string> + <string name="pref_title_post_filter">تصفية الخيوط</string> + <string name="pref_title_post_tabs">الخيط الرئيس</string> + <string name="pref_title_show_boosts">عرض المُعاد نشرها</string> + <string name="pref_title_show_replies">عرض الردود</string> + <string name="pref_title_show_media_preview">إظهار معاينات الوسائط</string> + <string name="pref_title_proxy_settings">البروكسي</string> + <string name="pref_title_http_proxy_settings">بروكسي HTTP</string> + <string name="pref_title_http_proxy_enable">تفعيل بروكسي HTTP</string> + <string name="pref_title_http_proxy_server">خادوم بروكسي HTTP</string> + <string name="pref_title_http_proxy_port">منفذ بروكسي HTTP</string> + <string name="pref_default_post_privacy">الخصوصية الإفتراضية للنشر</string> + <string name="pref_default_media_sensitivity">تعيين الوسائط كمحتوى حساس دائمًا</string> + <string name="pref_publishing">النشر</string> + <string name="pref_failed_to_sync">فَشِلَت عملية مزامنة الخيارات</string> + <string name="post_privacy_public">للعامة</string> + <string name="post_privacy_unlisted">غير مدرج</string> + <string name="post_privacy_followers_only">للمُتابِعين فقط</string> + <string name="pref_post_text_size">حجم الخط</string> + <string name="post_text_size_smallest">صغير جدا</string> + <string name="post_text_size_small">صغير</string> + <string name="post_text_size_medium">متوسط</string> + <string name="post_text_size_large">عريض</string> + <string name="post_text_size_largest">عريض جدا</string> + <string name="notification_mention_name">إشارات جديدة</string> + <string name="notification_mention_descriptions">الإخطارات عندما يشار إليك</string> + <string name="notification_follow_name">متابِعون جدد</string> + <string name="notification_follow_description">إخطارات عند تلقي متابعين جدد</string> + <string name="notification_boost_name">المشاركات</string> + <string name="notification_boost_description">إشعاري عندما يقوم أحدهم بإعادة نشر منشوراتي</string> + <string name="notification_favourite_name">المفضلة</string> + <string name="notification_favourite_description">الإشعار عندما يقوم أحدهم بإضافة منشوراتك إلى مفضلاته</string> + <string name="notification_mention_format">%1$s أشار إليك</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s و %4$d آخرون</string> + <string name="notification_summary_medium">%1$s, %2$s, و %3$s</string> + <string name="notification_summary_small">%1$s و %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="zero">%1$d تفاعلات جديدة</item> + <item quantity="one">تفاعل جديد</item> + <item quantity="two">تفاعلين جديدين</item> + <item quantity="few">%1$d تفاعلات جديدة</item> + <item quantity="many">%1$d تفاعلات جديدة</item> + <item quantity="other">%1$d تفاعلات جديدة</item> + </plurals> + <string name="description_account_locked">حساب مقفل</string> + <string name="about_title_activity">عن التطبيق</string> + <string name="about_tusky_version">توسكي %1$s</string> + <string name="about_tusky_license">Tuksy برنامج حر و مفتوح المصدر. مطور تحت رخصة GNU General Public License Version 3. يمكنكم الإطلاع على الرخصة على : https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">موقع ويب المشروع: https://tusky.app</string> + <string name="about_bug_feature_request_site">تقارير الأخطاء وطلبات التحسينات: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">الملف الشخصي لتوسكي</string> + <string name="post_share_content">شارك محتوى التبويق</string> + <string name="post_share_link">شارك الرابط إلى التبويق</string> + <string name="post_media_images">صور</string> + <string name="post_media_video">فيديو</string> + <string name="state_follow_requested">طلب متابعة</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">في %1$dy</string> + <string name="abbreviated_in_days">في %1$dd</string> + <string name="abbreviated_in_hours">في %1$dh</string> + <string name="abbreviated_in_minutes">في %1$dm</string> + <string name="abbreviated_in_seconds">في %1$ds</string> + <string name="abbreviated_years_ago">%1$dس</string> + <string name="abbreviated_days_ago">%1$dأيام</string> + <string name="abbreviated_hours_ago">%1$dسا</string> + <string name="abbreviated_minutes_ago">%1$dد</string> + <string name="abbreviated_seconds_ago">%1$dثا</string> + <string name="follows_you">يتبعك الآن</string> + <string name="pref_title_alway_show_sensitive_media">أظهر دائما المحتوى الحساس</string> + <string name="title_media">الوسائط</string> + <string name="replying_to">ردًا على @%1$s</string> + <string name="load_more_placeholder_text">حمِّل المزيد</string> + <string name="pref_title_public_filter_keywords">الخطوط الزمنية العمومية</string> + <string name="pref_title_thread_filter_keywords">المحادثات</string> + <string name="filter_addition_title">إضافة عامل تصفية</string> + <string name="filter_edit_title">تعديل عامل التصفية</string> + <string name="filter_dialog_remove_button">إزالة</string> + <string name="filter_dialog_update_button">تحديث</string> + <string name="filter_add_description">العبارة التي يلزم تصفيتها</string> + <string name="add_account_name">إضافة حساب</string> + <string name="add_account_description">إضافة حساب ماستدون جديد</string> + <string name="action_lists">القوائم</string> + <string name="title_lists">القوائم</string> + <string name="error_create_list">لا يمكن إنشاء قائمة</string> + <string name="error_rename_list">تعذّر تحديث القائمة</string> + <string name="error_delete_list">لا يمكن حذف القائمة</string> + <string name="action_create_list">إنشاء قائمة</string> + <string name="action_rename_list">تحديث القائمة</string> + <string name="action_delete_list">حذف القائمة</string> + <string name="hint_search_people_list">البحث عن أشخاص قصد متابعتهم</string> + <string name="action_add_to_list">إضافة الحساب إلى القائمة</string> + <string name="action_remove_from_list">إزالة الحساب مِن القائمة</string> + <string name="compose_active_account_description">النشر بإسم %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="zero">وصف لضعاف البصر +\n(%1$d أحرف على أقصى تقدير)</item> + <item quantity="one">وصف لضعاف البصر +\n(حرف واحد على أقصى تقدير)</item> + <item quantity="two">وصف لضعاف البصر +\n(حرفان على أقصى تقدير)</item> + <item quantity="few">وصف لضعاف البصر +\n(%1$d حروف على أقصى تقدير)</item> + <item quantity="many">وصف لضعاف البصر +\n(%1$d حرفًا على أقصى تقدير)</item> + <item quantity="other">وصف لضعاف البصر +\n(%1$d حرف على أقصى تقدير)</item> + </plurals> + <string name="action_set_caption">إضافة شرح</string> + <string name="action_remove">حذف</string> + <string name="lock_account_label">تجميد الحساب</string> + <string name="lock_account_label_description">يتطلب منك قبول طلبات المتابَعة يدويا</string> + <string name="compose_save_draft">هل تود الإحتفاظ بالمسودة ؟</string> + <string name="send_post_notification_title">جارٍ إرسال التبويق…</string> + <string name="send_post_notification_error_title">خطأ أثناء عملية إرسال التبويق</string> + <string name="send_post_notification_channel_name">إرسال المنشورات</string> + <string name="send_post_notification_cancel_title">أُلغيَ الإرسال</string> + <string name="send_post_notification_saved_content">تم الاحتفاظ بنسخة مِن التبويق في مسوداتك</string> + <string name="action_compose_shortcut">حرر</string> + <string name="error_no_custom_emojis">لا يحتوي مثيل خادومكم %1$s على أية حزمة إيموجي مخصصة</string> + <string name="emoji_style">نوع الإيموجي</string> + <string name="system_default">الإفتراضي في النظام</string> + <string name="download_fonts">يجب عليك أولا تنزيل حزمة الإيموجي هذه</string> + <string name="performing_lookup_title">البحث جارٍ …</string> + <string name="expand_collapse_all_posts">توسيع/طي كافة المنشورات</string> + <string name="action_open_post">افتح التبويق</string> + <string name="restart_required">مطلوب إعادة تشغيل التطبيق</string> + <string name="restart_emoji">إعادة تشغيل توسكي مطلوبة قصد تفعيل التعديلات</string> + <string name="later">لاحقًا</string> + <string name="restart">إعادة التشغيل</string> + <string name="caption_systememoji">مجموعة الإيموجي المُدمَجة في جهازك</string> + <string name="caption_blobmoji">إيموجيات الفقاقيع المعروفة على أندرويد 4.4–7.1</string> + <string name="caption_twemoji">مجموعة الإيموجي الخاصة بماستدون</string> + <string name="download_failed">فشلت عملية التنزيل</string> + <string name="profile_badge_bot_text">روبوت</string> + <string name="account_moved_description">إنتقَلَ %1$s إلى :</string> + <string name="reblog_private">إعادة النشر إلى الجمهور الأصلي</string> + <string name="unreblog_private">إلغاء إعادة النشر</string> + <string name="license_description">يحتوي توسكي على شيفرة وأوصول صادرة مِن المشاريع المفتوحة التالية :</string> + <string name="license_apache_2">مرخص تحت رخصة أباتشي (النسخة أسفله)</string> + <string name="license_cc_by_4">المشاع الإبداعي CC-BY 4.0</string> + <string name="license_cc_by_sa_4">المشاع الإبداعي CC-BY-SA 4.0</string> + <string name="profile_metadata_label">البيانات الوصفية للملف الشخصي</string> + <string name="profile_metadata_add">إضافة بيانات</string> + <string name="profile_metadata_label_label">الوسم</string> + <string name="profile_metadata_content_label">المحتوى</string> + <string name="pref_title_absolute_time">استخدام الوقت المطلق</string> + <string name="label_remote_account">المعلومات الواردة أدناه قد لا تعكس الملف الكامل للمستخدم. اضغط لفتحه على المتصفح.</string> + <string name="unpin_action">فك التدبيس</string> + <string name="pin_action">ثبت</string> + <string name="title_reblogged_by">شوركت بواسطة</string> + <string name="title_favourited_by">قام بإضافته إلى المفضلة</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s و %2$s</string> + <string name="conversation_more_recipients">%1$s و %2$s و %3$d آخَرون</string> + <string name="description_post_media">الوسائط: %1$s </string> + <string name="description_post_cw">تحذير عن المحتوى: %1$s </string> + <string name="description_post_media_no_description_placeholder">مِن دون وصف </string> + <string name="description_post_reblogged">أعاد تدوينه </string> + <string name="description_visibility_public">للعامة </string> + <string name="description_visibility_unlisted">غير مُدرَج </string> + <string name="description_visibility_private">المتابِعون </string> + <string name="description_visibility_direct">مباشر </string> + <string name="hint_list_name">اسم القائمة</string> + <string name="action_delete_and_redraft">حذف وإعادة الصياغة</string> + <string name="action_open_reblogger">إظهار صاحب إعادة النشر</string> + <string name="action_open_media_n">افتح الوسيط #%1$d</string> + <string name="download_media">نزّل الوسائط</string> + <string name="downloading_media">جارٍ تنزيل الوسائط</string> + <string name="dialog_redraft_post_warning">هل تريد حذف وإعادة صياغة هذا التبويق؟</string> + <string name="description_post_favourited">تم تفضيله</string> + <string name="edit_hashtag_hint">وسم بدون #</string> + <string name="notifications_clear">مسح</string> + <string name="notifications_apply_filter">عامل تصفية</string> + <string name="filter_apply">طَبِّق</string> + <string name="compose_shortcut_long_label">تحرير منشور</string> + <string name="compose_shortcut_short_label">كتابة</string> + <string name="notification_clear_text">هل تريد حقا مسح كافة إشعاراتك؟</string> + <string name="poll_info_time_absolute">ينتهي في %1$s</string> + <string name="poll_info_closed">انتهى</string> + <string name="poll_vote">صَوِّت</string> + <string name="notification_poll_name">استطلاعات الرأي</string> + <string name="notification_poll_description">الإشعارات المتعلقة باستطلاعات الرأي التي انتهت</string> + <plurals name="poll_info_votes"> + <item quantity="zero">%1$s صوت</item> + <item quantity="one">%1$s صوت</item> + <item quantity="two">صوتين</item> + <item quantity="few">%1$s أصوات</item> + <item quantity="many">%1$s أصوات</item> + <item quantity="other">%1$s أصوات</item> + </plurals> + <string name="poll_ended_voted">لقد انتهى استطلاع رأي قد أبديت بصوتك فيه</string> + <string name="poll_ended_created">لقد انتهى استطلاع رأي قمتَ بإنشائه</string> + <string name="pref_title_notification_filter_poll">تنتهي استطلاعات الرأي</string> + <plurals name="favs"> + <item quantity="zero"><b>%1$s</b> مفضلة</item> + <item quantity="one"><b>%1$s</b> مفضلة</item> + <item quantity="two"><b>%1$s</b> مفضلتين</item> + <item quantity="few"><b>%1$s</b> مفضلة</item> + <item quantity="many"><b>%1$s</b> مفضلات</item> + <item quantity="other"><b>%1$s</b> مفضلات</item> + </plurals> + <plurals name="reblogs"> + <item quantity="zero">لا إعادة نشر</item> + <item quantity="one">إعادة نشر واحدة</item> + <item quantity="two">أعيد نشره مرّتان</item> + <item quantity="few"><b>%1$s</b> إعادات نشر</item> + <item quantity="many"><b>%1$s</b> إعادة نشر</item> + <item quantity="other"><b>%1$s</b> إعادة نشر</item> + </plurals> + <string name="pref_title_bot_overlay">إظهار علامة البوتات</string> + <plurals name="poll_timespan_days"> + <item quantity="zero">%1$d أيام متبقية</item> + <item quantity="one">%1$d يوم متبقي</item> + <item quantity="two">%1$d يومين متبقيين</item> + <item quantity="few">%1$d أيام متبقية</item> + <item quantity="many">%1$d أيام متبقية</item> + <item quantity="other">%1$d أيام متبقية</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="zero">%1$d ساعات متبقية</item> + <item quantity="one">%1$d ساعة متبقية</item> + <item quantity="two">%1$d ساعتان متبقيتان</item> + <item quantity="few">%1$d ساعات متبقية</item> + <item quantity="many">%1$d ساعات متبقية</item> + <item quantity="other">%1$d ساعات متبقية</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="zero">%1$d دقائق متبقية</item> + <item quantity="one">%1$d دقيقة متبقية</item> + <item quantity="two">%1$d دقيقتان متبقيتان</item> + <item quantity="few">%1$d دقائق متبقية</item> + <item quantity="many">%1$d دقائق متبقية</item> + <item quantity="other">%1$d دقائق متبقية</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="zero">%1$d ثوان متبقية</item> + <item quantity="one">%1$d ثانية متبقية</item> + <item quantity="two">%1$d ثانيتان متبقيتان</item> + <item quantity="few">%1$d ثوان متبقية</item> + <item quantity="many">%1$d ثوان متبقية</item> + <item quantity="other">%1$d ثوان متبقية</item> + </plurals> + <string name="compose_preview_image_description">إجراءات على الصورة %1$s</string> + <string name="caption_notoemoji">حزمة الإيموجي الحالية لـ غوغل</string> + <string name="title_domain_mutes">النطاقات المخفية</string> + <string name="action_view_domain_mutes">النطاقات المخفية</string> + <string name="action_mute_domain">اكتم %1$s</string> + <string name="confirmation_domain_unmuted">لم يعُد %1$s مخفيا</string> + <string name="mute_domain_warning_dialog_ok">اخفِ كافة النطاق</string> + <string name="pref_title_animate_gif_avatars">تنشيط الصور المتحركة GIF للحسابات</string> + <string name="button_continue">واصل</string> + <string name="button_back">العودة</string> + <string name="button_done">تم</string> + <string name="report_sent_success">تم الابلاغ عن @%1$s بنجاح</string> + <string name="hint_additional_info">تعليقات إضافية</string> + <string name="report_remote_instance">أعد تحويله إلى %1$s</string> + <string name="failed_report">فشل الابلاغ</string> + <string name="failed_fetch_posts">فشلت عملية جلب المنشورات</string> + <string name="filter_dialog_whole_word">الكلمة كاملة</string> + <string name="description_poll">استطلاع رأي بالخيارات: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="mute_domain_warning">هل أنت متأكد من أنك تريد حجب كافة %1$s؟ سوف لن يكون باستطاعتك رؤية أي محتوى قادم من هذا النطاق بعد الآن ، لا في الخيوط الزمنية العامة ولا في إخطاراتك. سيتم إزالة متابِعيك الذين هم على هذا النطاق.</string> + <string name="report_description_1">سيتم إرسال التقرير إلى مشرفي خادمك. يمكنك تقديم تفسير عن سبب الإبلاغ عن الحساب أدناه:</string> + <string name="report_description_remote_instance">هذا الحساب ينتسب إلى خادم آخر. هل تريد إرسال نسخة مجهولة من التقرير إلى هناك أيضا؟</string> + <string name="title_accounts">الحسابات</string> + <string name="failed_search">فشل البحث</string> + <string name="action_add_poll">إضافة استطلاع رأي</string> + <string name="pref_title_alway_open_spoiler">افتح دائما المنشورات التي تحتوي على محتوى حساس</string> + <string name="create_poll_title">استطلاع رأي</string> + <string name="duration_5_min">5 دقائق</string> + <string name="duration_30_min">30 دقيقة</string> + <string name="duration_1_hour">ساعة واحدة</string> + <string name="duration_6_hours">6 ساعات</string> + <string name="duration_1_day">يوم واحد</string> + <string name="duration_3_days">3 أيام</string> + <string name="duration_7_days">7 أيام</string> + <string name="add_poll_choice">ضف خيارا</string> + <string name="poll_allow_multiple_choices">خيارات متعددة</string> + <string name="poll_new_choice_hint">الخيار %1$d</string> + <string name="edit_poll">تعديل</string> + <string name="filter_dialog_whole_word_description">عندما تكون الكلمة أو العبارة أبجدية رقمية فقط ، فلن يتم تطبيقها إلا إذا كانت مطابقة للكلمة بأكملها</string> + <string name="poll_info_format"> <!-- 15 votes • 1 hour left --> %1$s • %2$s</string> + <string name="title_scheduled_posts">المنشورات المُبَرمَجة</string> + <string name="action_edit">تعديل</string> + <string name="action_access_scheduled_posts">المنشورات المُبَرمَجة</string> + <string name="action_schedule_post">برمجة تبويق</string> + <string name="action_reset_schedule">صفّر</string> + <string name="post_lookup_error_format">خطأ أثناء البحث عن منشور %1$s</string> + <string name="title_bookmarks">الفواصل المرجعية</string> + <string name="action_bookmark">أضفه إلى الفواصل المرجعية</string> + <string name="action_view_bookmarks">الفواصل المرجعية</string> + <string name="about_powered_by_tusky">مدعوم بِـ Tusky</string> + <string name="description_post_bookmarked">أضيف إلى الفواصل المرجعية</string> + <string name="select_list_title">اختر قائمة</string> + <string name="list">القائمة</string> + <string name="no_drafts">ليس لديك أية مسودات.</string> + <string name="no_scheduled_posts">ليس لديك أية منشورات مُبرمَجة للنشر.</string> + <string name="warning_scheduling_interval">تُقدّر أدنى فترة لبرمجة النشر في ماستدون بـ 5 دقائق.</string> + <string name="pref_title_enable_swipe_for_tabs">تمكين حركات السحب للانتقال بين الألسنة</string> + <string name="pref_title_notification_filter_follow_requests">طلب متابعة</string> + <string name="pref_title_confirm_reblogs">أظهر نافذة للتأكيد قبل إعادة النشر</string> + <string name="pref_title_show_cards_in_timelines">أظهر معاينات الروابط على الخيوط</string> + <plurals name="poll_info_people"> + <item quantity="zero">لا شخص</item> + <item quantity="one">شخص واحد</item> + <item quantity="two">شخصان</item> + <item quantity="few">%1$s أشخاص</item> + <item quantity="many">%1$s أشخاص</item> + <item quantity="other">%1$s أشخاص</item> + </plurals> + <string name="notification_follow_request_description">إشعارات عن طلبات المتابعة</string> + <string name="notification_follow_request_name">طلبات المتابَعة</string> + <string name="dialog_mute_warning">أتريد كتم @%1$s؟</string> + <string name="dialog_block_warning">حجب @%1$s؟</string> + <string name="action_unmute_conversation">ألغِ كتم المحادثة</string> + <string name="action_mute_conversation">اكتم المحادثة</string> + <string name="notification_follow_request_format">%1$s طلبَ متابعتك</string> + <string name="hashtags">الوسوم</string> + <string name="add_hashtag_title">إضافة وسم</string> + <string name="pref_title_gradient_for_media">اظهر ألوانا متدرّجة للوسائط المخفية</string> + <string name="pref_main_nav_position">موضع شريط التنقل الرئيسي</string> + <string name="pref_main_nav_position_option_bottom">الأسفل</string> + <string name="pref_main_nav_position_option_top">الأعلى</string> + <string name="action_unmute_domain">إلغاء كتم %1$s</string> + <string name="dialog_mute_hide_notifications">أخفِ الإشعارات</string> + <string name="action_unmute_desc">إلغاء كتم %1$s</string> + <string name="pref_title_hide_top_toolbar">إخفاء عنوان شريط الأدوات العلوي</string> + <string name="title_announcements">الاعلانات</string> + <string name="dialog_delete_list_warning">أتريد حقا حذف القائمة %1$s؟</string> + <string name="drafts_post_failed_to_send">فشلت عملية إرسال التبويق!</string> + <string name="draft_deleted">حُذفَت المسودة</string> + <string name="action_subscribe_account">اشترك</string> + <string name="action_unsubscribe_account">إلغاء الإشتراك</string> + <string name="notification_subscription_format">نشر %1$s للتوّ</string> + <string name="action_delete_conversation">احذف المحادثة</string> + <string name="dialog_delete_conversation_warning">هل تريد حذف هذه المحادثة؟</string> + <string name="notification_subscription_name">منشورات جديدة</string> + <string name="post_media_attachments">مرفقات</string> + <string name="label_duration">المدة</string> + <string name="no_announcements">لا توجد إعلانات.</string> + <string name="account_note_hint">ملاحظتك الخاصة عن هذا الحساب</string> + <string name="account_note_saved">تم حفظها!</string> + <string name="review_notifications">راجع الإشعارات</string> + <string name="post_media_audio">صوت</string> + <string name="drafts_post_reply_removed">لقد حُذِف التبويق الذي حررت من أجله مسودة الرد</string> + <string name="pref_title_notification_filter_subscriptions">شخص ما أنا مشترك في حسابه قد نشر منشورا جديدا</string> + <string name="pref_title_animate_custom_emojis">حرّك الإيموجيات المخصصة</string> + <string name="wellbeing_hide_stats_posts">إخفاء الإحصائيات الكمية عن المنشورات</string> + <string name="wellbeing_hide_stats_profile">إخفاء الإحصائيات الكمية عن الملفات التعريفية</string> + <string name="action_unbookmark">احذف الفاصلة المرجعية</string> + <string name="pref_title_confirm_favourites">إظهار تأكيد قبل الإضافة إلى المفضلة</string> + <string name="drafts_failed_loading_reply">فشل في تحميل معلومات الرد</string> + <string name="duration_indefinite">غير محددة</string> + <string name="limit_notifications">وضع حد لإشعارات الخيط الزمني</string> + <string name="duration_14_days">14 يومًا</string> + <string name="duration_30_days">30 يومًا</string> + <string name="duration_60_days">60 يومًا</string> + <string name="duration_90_days">90 يومًا</string> + <string name="duration_180_days">180 يومًا</string> + <string name="duration_365_days">365 يومًا</string> + <string name="tusky_compose_post_quicksetting_label">تحرير منشور</string> + <string name="notification_sign_up_name">حسابات جديدة</string> + <string name="title_login">تسجيل الدخول</string> + <string name="notification_sign_up_format">قام %1$s بإنشاء حساب</string> + <string name="pref_title_notification_filter_sign_ups">أحدهم أنشأ حسابا جديدا</string> + <string name="notification_update_name">منشورات تم تعديلها</string> + <string name="notification_update_format">قام %1$s بتعديل منشوره</string> + <string name="a11y_label_loading_thread">تحميل خيط المحادثة</string> + <string name="hint_media_description_missing">يجب أن تضع وصفًا للوسائط.</string> + <string name="error_multimedia_size_limit">لا يمكن أن يتجاوز حجم ملفات الفيديو والصوت %1$s ميغا بايت.</string> + <string name="error_image_edit_failed">لا يمكن تحرير الصورة.</string> + <string name="title_edits">التعديلات</string> + <string name="pref_title_notification_filter_reports">هناك شكوى جديدة</string> + <string name="pref_show_self_username_never">أبداً</string> + <string name="notification_subscription_description">تشغيل الاشعارات عندما يقوم شخص انت مشترك معه بنشر منشور جديد</string> + <string name="notification_update_description">ارسال إشعار عند تحرير المنشورات التي تفاعلت معها</string> + <string name="notification_report_name">شكاوي</string> + <string name="notification_report_description">ارسال إشعار عن شكاوى المدراء</string> + <string name="set_focus_description">اضغط على الدائرة أو اسحبها لاختيار النقطة المحورية التي ستكون مرئية دائمًا في الصور المصغرة.</string> + <string name="compose_save_draft_loses_media">حفظ المسودة؟ (سيتم رفع المرفقات مرة أخرى عند استعادة المسودة.)</string> + <string name="compose_unsaved_changes">لديك تعديلات لم تحفظ.</string> + <string name="no_lists">ليس لديك أي قوائم.</string> + <string name="post_edited">عدَّلَ %1$s</string> + <string name="notification_report_format">شكوى جديدة عن %1$s</string> + <string name="status_edit_info">عدّله %1$s</string> + <string name="status_created_info">أنشأه %1$s</string> + <string name="wellbeing_mode_notice">سيتم إخفاء بعض المعلومات التي قد تؤثر على صحتك العقلية. هذا يتضمن: +\n +\n- المفضلة/المشاركات/متابعة الاشعارات +\n- المفضلة/عدد المشاركات على المنشور +\n- إحصائيات المتابعين/و الاحصائات على المنشورات في الملفات الشخصية +\n +\nلن تتأثر الاشعارات، ولكن يمكنك مراجعة اعدادات الاشعارات يدويا.</string> + <string name="confirmation_hashtag_unfollowed">#%1$s الغاء متابعة</string> + <string name="pref_title_http_proxy_port_message">المنفذ (port) يجب ان يكون بين %2$d و %1$d</string> + <string name="action_add_or_remove_from_list">إضافة أو إزالة من القائمة</string> + <string name="failed_to_add_to_list">فشل إضافة الحساب إلى القائمة</string> + <string name="description_post_edited">تم تعديله</string> + <string name="duration_no_change">(لا تعديل)</string> + <string name="pref_title_show_self_username">إظهار اسم المستخدم في أشرطة الأدوات</string> + <string name="follow_requests_info">على الرغم ان حسابك غير مقفل، اعتقد فريق %1$s أنك قد ترغب في مراجعة طلبات المتابعة من هذه الحسابات يدويًا.</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="zero">لا يمكنك رفع أكثر من %1$d مرفق.</item> + <item quantity="one">لا يمكنك رفع أكثر من %1$d مرفق.</item> + <item quantity="two">لا يمكنك رفع أكثر من %1$d مرفقان.</item> + <item quantity="few">لا يمكنك رفع أكثر من %1$d مرفقات.</item> + <item quantity="many">لا يمكنك رفع أكثر من %1$d مرفقات.</item> + <item quantity="other">لا يمكنك رفع أكثر من %1$d مرفقات.</item> + </plurals> + <string name="saving_draft">جار حفظ المسودة …</string> + <string name="dialog_push_notification_migration_other_accounts">لقد قمت بإعادة تسجيل الدخول إلى حسابك الجاري لمنح إذن الاشعارات لـ Tusky. ومع ذلك، لا يزال لديك حسابات أخرى لم يتم ترحيلها بهذه الطريقة. قم بالتبديل إليهم وإعادة تسجيل الدخول واحدًا تلو الآخر لتمكين دعم إشعارات UnifiedPush.</string> + <string name="instance_rule_title">%1$s شروط</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="pref_default_post_language">لغة النشر الافتراضية</string> + <string name="status_count_one_plus">1+</string> + <string name="report_category_spam">مزعجة</string> + <string name="report_category_other">اخرى</string> + <string name="error_muting_hashtag_format">خطأ في كتم صوت %1$s#</string> + <string name="error_unmuting_hashtag_format">خطأ في إلغاء كتم صوت %1$s#</string> + <string name="notification_sign_up_description">ارسال إشعار عندما يقوم مستخدم جديد بالتسجيل</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="failed_to_unpin">فشل إلغاء التثبيت</string> + <string name="error_following_hashtags_unsupported">مثيل الخادم هذا لا يدعم متابعة الوسوم.</string> + <string name="action_discard">تجاهل التعديلات</string> + <string name="action_continue_edit">الاستمرار في التعديل</string> + <string name="pref_show_self_username_always">دائماً</string> + <string name="post_media_alt">ALT</string> + <string name="notification_header_report_format">%1$s أبلغ عن %2$s</string> + <string name="notification_summary_report_format">%1$s · %2$d منشورات ملحقة</string> + <string name="failed_to_pin">فشل التثبيت</string> + <string name="instance_rule_info">بتسجيل الدخول، أنت توافق على %1$s.</string> + <string name="description_post_language">لغة المنشور</string> + <string name="delete_scheduled_post_warning">هل تريد حذف هذا المنشور المُبَرمَج؟</string> + <string name="tips_push_notification_migration">أعد تسجيل الدخول إلى جميع الحسابات لتمكين دعم الإشعارات.</string> + <string name="action_set_focus">ضبط نقطة التركيز</string> + <string name="action_edit_image">تعديل الصورة</string> + <string name="action_add_reaction">إضافة رد فعل</string> + <string name="action_share_account_link">مشاركة رابط الحساب</string> + <string name="action_share_account_username">مشاركة اسم مستخدم الحساب</string> + <string name="send_account_link_to">شارك رابط الحساب الى…</string> + <string name="send_account_username_to">شارك اسم مستخدم الحساب إلى…</string> + <string name="account_username_copied">تم نسخ اسم المستخدم</string> + <string name="pref_show_self_username_disambiguate">عند تسجيل الدخول إلى حسابات متعددة</string> + <string name="status_created_at_now">الآن</string> + <string name="failed_to_remove_from_list">فشل إزالة الحساب من القائمة</string> + <string name="action_unfollow_hashtag_format">الغاء متابعة #%1$s ؟</string> + <string name="mute_notifications_switch">كتم الاشعارات</string> + <string name="error_following_hashtag_format">خطأ في متابعة %1$s#</string> + <string name="error_unfollowing_hashtag_format">خطأ في إلغاء متابعة %1$s#</string> + <string name="title_followed_hashtags">الوسوم المتابَعة</string> + <string name="error_status_source_load">فشل تحميل مصدر المنشور من الخادم.</string> + <string name="report_category_violation">انتهاك الشروط</string> + <string name="dialog_push_notification_migration">من أجل استخدام الإشعارات عبر UnifiedPush ، يحتاج Tusky إلى إذن للإشتراك في الإشعارات على خادم Mastodon الخاص بك. يتطلب هذا إعادة تسجيل الدخول لتغيير نطاقات OAuth الممنوحة لـ Tusky. سيؤدي استخدام خيار إعادة تسجيل الدخول هنا أو في اعدادات الحساب إلى الاحتفاظ بجميع المسودات المحلية وذاكرة التخزين المؤقت.</string> + <string name="pref_title_wellbeing_mode">الرفاهية</string> + <string name="account_date_joined">انضم في %1$s</string> + <string name="title_migration_relogin">أعد تسجيل الدخول لاستلام الاشعارات</string> + <string name="action_dismiss">تجاهل</string> + <string name="action_details">تفاصيل</string> + <string name="pref_title_notification_filter_updates">المنشور الذي تفاعلت معه تم تعديله</string> + <string name="error_loading_account_details">فشلت عملية تحميل تفاصيل الحساب</string> + <string name="error_could_not_load_login_page">فشل الوصول الى صفحة تسجيل الدخول.</string> + <string name="action_post_failed">فشل التحميل</string> + <string name="action_post_failed_show_drafts">إظهار المسودات</string> + <string name="action_post_failed_do_nothing">تخطي</string> + <string name="pref_title_reading_order">ترتيب القراءة</string> + <string name="pref_summary_http_proxy_disabled">مُعطَّل</string> + <string name="pref_reading_order_oldest_first">الأقدم أولاً</string> + <string name="pref_reading_order_newest_first">الأحدث أولاً</string> + <string name="action_browser_login">الولوج باستخدام متصفح</string> + <string name="title_public_trending_hashtags">الوسوم المتداولة</string> + <string name="accessibility_talking_about_tag">هناك %1$d أشخاص يتحدثون عن الوسم %2$s</string> + <string name="total_usage">الاستخدام الإجمالي</string> + <string name="ui_error_unknown">سبب مجهول</string> + <string name="socket_timeout_exception">طال الاتصال بخادمك كثيرًا</string> + <string name="action_add">إضافة</string> + <string name="dialog_follow_hashtag_title">تابِع الوسم</string> + <string name="dialog_follow_hashtag_hint">#وسم</string> + <string name="pref_ui_text_size">حجم خط واجهة المستخدم</string> + <string name="label_image">صورة</string> + <string name="about_account_info_title">حسابُك</string> + <string name="post_media_image">صورة</string> + <string name="about_copy">نسخ معلومات الإصدار والجهاز</string> + <string name="notification_listenable_worker_name">النشاط في الخلفية</string> + <string name="about_device_info_title">جهازك</string> + <string name="action_refresh">أنعِش</string> + <string name="notification_notification_worker">جارٍ جلب الإشعارات…</string> + <string name="app_theme_system_black">استخدم حُلّة النظام (أسوَد)</string> + <string name="notification_unknown_name">مجهول</string> + <string name="total_accounts">الحسابات</string> + <string name="list_exclusive_label">أخفيه مِن الخيط الرئيسي</string> + <string name="dialog_save_profile_changes_message">هل تريد حفط التغييرات التي طرأت على ملفك التعريفي؟</string> + <string name="dialog_delete_filter_text">أتريد حذف عامل التصفية ”%1$s“؟</string> + <string name="dialog_delete_filter_positive_action">حذف</string> + <string name="select_list_manage">إدارة القوائم</string> + <string name="status_filtered_show_anyway">أظهره على أي حال</string> + <string name="filter_description_hide">إخفاء بالكامل</string> + <string name="ui_success_accepted_follow_request">تم قبول طلب المتابَعة</string> + <string name="title_public_trending_statuses">المنشورات المتداوَلة</string> + <string name="label_filter_title">التسمية</string> + <string name="ui_success_rejected_follow_request">تم رفض طلب المتابَعة</string> + <string name="filter_action_warn">تحذير</string> + <string name="filter_action_hide">إخفاء</string> + <string name="filter_description_warn">إخفاء وراء تحذير</string> + <string name="filter_keyword_addition_title">إضافة كلمة مفتاحية</string> + <string name="filter_edit_keyword_title">تعديل الكلمة المفتاحية</string> + <string name="pref_title_account_filter_keywords">الملفات التعريفية</string> + <string name="pref_title_show_stat_inline">إظهار إحصائيات المنشورات في الخيوط الزمنية</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="error_media_upload_sending_fmt">فَشِلَ التحميل: %1$s</string> + <string name="load_newest_notifications">تحميل الإشعارات الجديدة</string> + <string name="compose_delete_draft">أتريد حذف المسودّة؟</string> + <string name="about_copied">تم نسخ معلومات الإصدار والجهاز</string> + <string name="list_reply_policy_none">لا أحد</string> + <string name="list_reply_policy_list">أعضاء القائمة</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-be/strings.xml b/app/src/main/res/values-be/strings.xml new file mode 100644 index 0000000..3df470d --- /dev/null +++ b/app/src/main/res/values-be/strings.xml @@ -0,0 +1,670 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_empty">Не можа быць пустым.</string> + <string name="error_invalid_domain">Уведзены недапушчальны дамен</string> + <string name="error_failed_app_registration">Памылка праверкі сапраўднасці на гэтым серверы. Калі праблема застаецца, паспрабуйце ўвайсці з браўзера з меню.</string> + <string name="error_no_web_browser_found">Браўзер не знойдзены.</string> + <string name="error_authorization_denied">Аўтарызацыя была адхілена. Калі ўпэўнены што ўвялі сапраўдныя ўліковыя даныя, паспрабуйце ўвайсці з браўзера з меню.</string> + <string name="error_loading_account_details">Дэталі ўліковага запісу не атрыманы</string> + <string name="error_generic">Здарылася памылка.</string> + <string name="error_network">Здарылася сеткавая памылка. Калі ласка, праверце злучэнне і паспрабуйце зноў.</string> + <string name="error_authorization_unknown">Адбылася невядомая памылка праверкі сапраўднасці. Калі праблема застаецца, паспрабуйце ўвайсці з браўзера з меню.</string> + <string name="error_retrieving_oauth_token">Токен уваходу не атрыманы. Калі праблема застаецца, паспрабуйце ўвайсці з браўзера з меню.</string> + <string name="error_compose_character_limit">Допіс занадта доўгі!</string> + <string name="error_multimedia_size_limit">Памер відэа- ды аўдыяфайлаў не можа перавышаць %1$s Мб.</string> + <string name="error_media_upload_type">Немагчыма запампаваць файл гэтага тыпу.</string> + <string name="error_media_upload_opening">Немагчыма адкрыць гэты файл.</string> + <string name="error_media_download_permission">Патрабуецца дазвол на захаванне медыя.</string> + <string name="error_media_upload_image_or_video">Немагчыма адначасова прымацаваць выяву ды відэа да допісу.</string> + <string name="error_media_upload_sending">Запампоўка не атрымалася.</string> + <string name="error_unfollowing_hashtag_format">Памылка адпіскі ад #%1$s</string> + <string name="error_following_hashtags_unsupported">Гэты сервер не падтрымлівае падпіску на хэштэгі.</string> + <string name="error_muting_hashtag_format">Памылка ігнаравання #%1$s</string> + <string name="error_could_not_load_login_page">Немагчыма загрузіць старонку ўваходу.</string> + <string name="error_image_edit_failed">Немагчыма рэдагаваць выяву.</string> + <string name="error_media_upload_permission">Патрабуецца дазвол на чытанне медыя.</string> + <string name="error_sender_account_gone">Памылка дасылання допісу.</string> + <string name="error_following_hashtag_format">Памылка падпіскі на #%1$s</string> + <string name="error_unmuting_hashtag_format">Памылка адмены ігнаравання #%1$s</string> + <string name="error_status_source_load">Не атрымалася загрузіць крыніцу статусу з сервера.</string> + <string name="title_login">Уваход</string> + <string name="title_home">Галоўная</string> + <string name="title_notifications">Апавяшчэнні</string> + <string name="title_public_local">Лакальная</string> + <string name="title_public_federated">Глабальная</string> + <string name="title_direct_messages">Асабістыя паведамленні</string> + <string name="title_tab_preferences">Укладкі</string> + <string name="title_view_thread">Ланцуг</string> + <string name="title_posts">Допісы</string> + <string name="title_posts_with_replies">З адказамі</string> + <string name="title_follows">Падпіскі</string> + <string name="title_followers">Падпісчыкі</string> + <string name="title_favourites">Абраныя</string> + <string name="title_bookmarks">Закладкі</string> + <string name="title_domain_mutes">Схаваныя дамены</string> + <string name="title_follow_requests">Запыты на падпіску</string> + <string name="title_edit_profile">Рэдагаваць профіль</string> + <string name="title_drafts">Чарнавікі</string> + <string name="title_scheduled_posts">Запланаваныя допісы</string> + <string name="title_posts_pinned">Замацаваныя</string> + <string name="title_mutes">Ігнаруемыя карыстальнікі</string> + <string name="title_blocks">Заблакаваныя карыстальнікі</string> + <string name="title_migration_relogin">Увайдзіце зноў, каб атрымліваць push-апавяшчэнні</string> + <string name="title_announcements">Аб\'явы</string> + <string name="title_licenses">Ліцэнзіі</string> + <string name="title_followed_hashtags">Падпіскі на хэштэгі</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s пашырыў(-ла)</string> + <string name="post_sensitive_media_title">Далікатнае змесціва</string> + <string name="post_media_hidden_title">Медыя схаванае</string> + <string name="post_content_show_more">Разгарнуць</string> + <string name="report_comment_hint">Дадатковыя каментарыі\?</string> + <string name="post_sensitive_media_directions">Націсніце для прагляду</string> + <string name="post_content_warning_show_more">Паказаць больш</string> + <string name="post_content_warning_show_less">Паказаць менш</string> + <string name="post_content_show_less">Згарнуць</string> + <string name="post_edited">Рэдагаваны %1$s</string> + <string name="message_empty">Тут нічога няма.</string> + <string name="footer_empty">Тут нічога няма. Пацягніце ўніз, каб абнавіць!</string> + <string name="notification_reblog_format">%1$s пашырыў(-ла) ваш допіс</string> + <string name="notification_favourite_format">%1$s падабаўся ваш допіс</string> + <string name="notification_follow_format">%1$s падпісаўся(-лася) на вас</string> + <string name="notification_follow_request_format">%1$s запытаў(-ла) падпіску на вас</string> + <string name="notification_sign_up_format">%1$s зарэгістраваўся(-лася)</string> + <string name="notification_subscription_format">Новы допіс ад %1$s</string> + <string name="notification_update_format">%1$s адрэдагаваў(-ла) свой допіс</string> + <string name="notification_report_format">Новая скарга на %1$s</string> + <string name="notification_header_report_format">%1$s скардзіцца на %2$s</string> + <string name="notification_summary_report_format">%1$s · %2$d допісаў дададзена</string> + <string name="report_username_format">Паскардзіцца на @%1$s</string> + <string name="action_quick_reply">Хуткі адказ</string> + <string name="action_reply">Адказаць</string> + <string name="post_media_alt">ALT</string> + <string name="action_reblog">Пашырыць</string> + <string name="action_unreblog">Скасаваць пашырэнне</string> + <string name="action_favourite">Упадабаць</string> + <string name="action_unfavourite">Выдаліць з абраных</string> + <string name="action_bookmark">Дадаць у закладкі</string> + <string name="action_delete">Выдаліць</string> + <string name="action_search">Пошук</string> + <string name="action_content_warning">Папярэджанне пра змесціва</string> + <string name="action_emoji_keyboard">Клавіятура эмодзі</string> + <string name="action_access_drafts">Чарнавікі</string> + <string name="action_access_scheduled_posts">Запланаваныя допісы</string> + <string name="action_toggle_visibility">Бачнасць допісаў</string> + <string name="action_hashtags">Хэштэгі</string> + <string name="action_open_reblogger">Адкрыць аўтара пашырэння</string> + <string name="action_open_reblogged_by">Паказаць пашырэнні</string> + <string name="action_open_faved_by">Паказаць абраныя</string> + <string name="title_hashtags_dialog">Хэштэгі</string> + <string name="title_mentions_dialog">Згадкі</string> + <string name="action_dismiss">Адхіліць</string> + <string name="action_details">Дэталі</string> + <string name="title_links_dialog">Спасылкі</string> + <string name="action_open_media_n">Адкрыць медыя #%1$d</string> + <string name="action_add_reaction">дадаць рэакцыю</string> + <string name="download_image">Спампоўка %1$s</string> + <string name="action_share_as">Абагуліць як…</string> + <string name="download_media">Спампаваць медыя</string> + <string name="downloading_media">Медыя пампуецца</string> + <string name="send_post_content_to">Падзяліцца допісам з…</string> + <string name="send_media_to">Падзяліцца медыя з…</string> + <string name="send_post_link_to">Падзяліцца URL допісу з…</string> + <string name="confirmation_reported">Даслана!</string> + <string name="confirmation_unblocked">Карыстальнік разблакаваны</string> + <string name="hint_domain">Які сервер\?</string> + <string name="hint_compose">Што адбываецца\?</string> + <string name="hint_content_warning">Папярэджанне пра змесціва</string> + <string name="hint_display_name">Бачнае імя</string> + <string name="dialog_title_finishing_media_upload">Заканчваецца запампоўка медыя</string> + <string name="visibility_public">Публічны: Дасылаць у публічныя стужкі</string> + <string name="visibility_unlisted">Не ў стужках: Не паказваць у публічных стужках</string> + <string name="visibility_private">Толькі падпісчыкі: Дасылаць толькі да падпісчыкаў</string> + <string name="pref_title_notification_alert_vibrate">Апавяшчаць з вібрацыяй</string> + <string name="pref_title_notification_alert_light">Апавяшчаць святлом</string> + <string name="pref_title_notification_filters">Апавяшчаць калі</string> + <string name="pref_title_notification_filter_mentions">згадалі</string> + <string name="pref_title_notification_filter_follows">падпісаліся</string> + <string name="pref_title_timeline_filters">Фільтры</string> + <string name="app_them_dark">Цёмная</string> + <string name="app_theme_light">Светлая</string> + <string name="app_theme_black">Чорная</string> + <string name="app_theme_auto">Аўтаматычна па захадзе сонца</string> + <string name="app_theme_system">Выкарыстоўваць сістэмную тэму</string> + <string name="pref_title_browser_settings">Браўзер</string> + <string name="pref_title_custom_tabs">Выкарыстоўваць укладкі браўзера Chrome</string> + <string name="pref_title_animate_gif_avatars">Анімаваць GIF-аватаркі</string> + <string name="pref_title_language">Мова</string> + <string name="pref_title_bot_overlay">Паказваць індыкатар для ботаў</string> + <string name="pref_title_gradient_for_media">Паказваць каляровыя градыенты замест скрытых медыя</string> + <string name="pref_title_animate_custom_emojis">Анімаваць уласныя эмодзі</string> + <string name="pref_title_post_filter">Фільтраванне стужкі</string> + <string name="pref_title_post_tabs">Укладкі</string> + <string name="pref_title_show_replies">Паказваць адказы</string> + <string name="pref_title_show_media_preview">Спампаваць медыя для наступнага прагляду</string> + <string name="pref_title_proxy_settings">Проксі</string> + <string name="pref_title_http_proxy_settings">HTTP проксі</string> + <string name="pref_title_http_proxy_enable">Уключыць HTTP проксі</string> + <string name="pref_title_http_proxy_server">HTTP проксі сервер</string> + <string name="pref_publishing">Публікацыя (сінхранізавана з серверам)</string> + <string name="pref_failed_to_sync">Не атрымалася сінхранізаваць налады</string> + <string name="pref_main_nav_position">Месца для галоўнай навігацыі</string> + <string name="pref_main_nav_position_option_top">Зверху</string> + <string name="pref_main_nav_position_option_bottom">Знізу</string> + <string name="post_privacy_public">Публічныя</string> + <string name="post_privacy_unlisted">Схаваныя</string> + <string name="post_privacy_followers_only">Толькі для падпісчыкаў</string> + <string name="post_text_size_small">Маленькі</string> + <string name="pref_show_self_username_disambiguate">Калі вы ўвайшлі ў некалькі ўліковых запісаў</string> + <string name="pref_show_self_username_never">Ніколі</string> + <string name="post_text_size_medium">Сярэдні</string> + <string name="post_text_size_large">Вялікі</string> + <string name="post_text_size_largest">Найбольшы</string> + <string name="pref_show_self_username_always">Заўсёды</string> + <string name="notification_follow_request_name">Запыты на падпіску</string> + <string name="notification_follow_request_description">Апавяшчаць аб запытах на падпіску</string> + <string name="notification_boost_name">Пашырэнні</string> + <string name="notification_boost_description">Апавяшчаць аб пашырэнні Вашых допісаў</string> + <string name="notification_favourite_name">Упадабаныя</string> + <string name="notification_report_name">Скаргі</string> + <string name="notification_report_description">Апавяшчаць аб мадэрацыйных скаргах</string> + <string name="notification_sign_up_name">Рэгістрацыі</string> + <string name="notification_sign_up_description">Апавяшчаць аб новых карыстальніках</string> + <string name="notification_update_name">Рэдагаванні допісу</string> + <string name="notification_update_description">Апавяшчаць, калі допісы, з якімі Вы ўзаемадзейнічалі, рэдагаваны</string> + <plurals name="notification_title_summary"> + <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="description_account_locked">Заблакаваны ўліковы запіс</string> + <string name="dialog_whats_an_instance">Сюды можна ўвесці адрас або дамен любога сервера, напрыклад mastodon.social, icosahedron.website, social.tchncs.de ды <a href="https://instances.social">больш!</a> +\n +\nКалі ў вас яшчэ няма ўліковага запісу, вы можаце ўвесці назву сервера, да якога вы жадаеце далучыцца, і стварыць там уліковы запіс. +\n +\nСервер — гэта месца, дзе размяшчаецца ваш уліковы запіс, але вы можаце лёгка размаўляць з людзьмі і падпісвацца на іх на іншых серверах, нібыта вы знаходзіцеся на тым жа сайце. +\n +\nПадрабязней на <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="action_unbookmark">Выдаліць з закладак</string> + <string name="action_more">Разгарнуць</string> + <string name="action_compose">Напісаць</string> + <string name="action_login">Увайсці з Tusky</string> + <string name="action_logout">Выхад</string> + <string name="action_logout_confirm">Вы ўпэўнены, што жадаеце выйсці з уліковага запісу %1$s\? У выніку будуць выдалены ўсе лакальныя даныя уліковага запісу разам з чарнавікамі і наладамі.</string> + <string name="action_follow">Падпісацца</string> + <string name="action_unfollow">Адпісацца</string> + <string name="action_block">Заблакаваць</string> + <string name="action_unblock">Разблакаваць</string> + <string name="action_hide_reblogs">Схаваць пашырэнні</string> + <string name="action_show_reblogs">Паказаць пашырэнні</string> + <string name="action_report">Паскардзіцца</string> + <string name="action_edit">Рэдагаваць</string> + <string name="action_delete_conversation">Выдаліць размову</string> + <string name="action_delete_and_redraft">Выдаліць і перастварыць</string> + <string name="action_send">ПАШЫРЫЦЬ</string> + <string name="action_send_public">ПАШЫРЫЦЬ!</string> + <string name="action_retry">Паўтарыць</string> + <string name="action_close">Закрыць</string> + <string name="action_view_profile">Профіль</string> + <string name="action_view_preferences">Налады</string> + <string name="action_view_account_preferences">Налады ўліковага запісу</string> + <string name="action_view_favourites">Абраныя</string> + <string name="action_view_bookmarks">Закладкі</string> + <string name="action_view_mutes">Ігнаруемыя карыстальнікі</string> + <string name="action_view_blocks">Заблакаваныя карыстальнікі</string> + <string name="action_view_domain_mutes">Схаваныя дамены</string> + <string name="action_view_follow_requests">Запыты на падпіску</string> + <string name="action_view_media">Медыя</string> + <string name="action_open_in_web">Адкрыць у браўзеры</string> + <string name="action_add_media">Дадаць медыя</string> + <string name="action_add_poll">Дадаць апытанне</string> + <string name="action_photo_take">Зрабіць здымак</string> + <string name="action_share">Абагуліць</string> + <string name="action_mute">Ігнараваць</string> + <string name="action_unmute">Скасаваць ігнараванне</string> + <string name="action_unmute_desc">Скасаваць ігнараванне %1$s</string> + <string name="action_mute_domain">Ігнараваць %1$s</string> + <string name="action_unmute_domain">Адмяніць ігнараванне %1$s</string> + <string name="action_mute_conversation">Ігнараваць размову</string> + <string name="action_unmute_conversation">Паказаць размову</string> + <string name="action_mention">Згадаць</string> + <string name="action_hide_media">Схаваць медыя</string> + <string name="action_open_drawer">Адкрыць меню</string> + <string name="action_save">Захаваць</string> + <string name="action_edit_profile">Рэдагаваць профіль</string> + <string name="action_edit_own_profile">Рэдагаваць</string> + <string name="action_undo">Скасаваць</string> + <string name="action_accept">Прыняць</string> + <string name="action_reject">Адхіліць</string> + <string name="action_schedule_post">Запланаваць допіс</string> + <string name="action_reset_schedule">Скінуць</string> + <string name="action_add_tab">Дадаць укладку</string> + <string name="action_links">Спасылкі</string> + <string name="action_mentions">Згадкі</string> + <string name="action_copy_link">Капіяваць спасылку</string> + <string name="action_open_as">Адкрыць як %1$s</string> + <string name="confirmation_unmuted">Прыхоўванне карыстальніка скасавана</string> + <string name="confirmation_domain_unmuted">%1$s адкрыты</string> + <string name="confirmation_hashtag_unfollowed">адпісаны(-ая) ад #%1$s</string> + <string name="hint_note">Пра сябе</string> + <string name="hint_search">Пошук…</string> + <string name="hint_media_description_missing">Медыя павінна мець апісанне.</string> + <string name="search_no_results">Няма рэзультатаў</string> + <string name="label_quick_reply">Адказаць…</string> + <string name="label_avatar">Аватар</string> + <string name="label_header">Загаловак</string> + <string name="link_whats_an_instance">Што такое сервер\?</string> + <string name="login_connection">Злучэнне…</string> + <string name="dialog_message_uploading_media">Запампоўваецца…</string> + <string name="dialog_download_image">Спампаваць</string> + <string name="dialog_message_cancel_follow_request">Адклікаць запыт на падпіску\?</string> + <string name="dialog_unfollow_warning">Адпісацца ад гэтага ўліковага запісу\?</string> + <string name="dialog_delete_post_warning">Выдаліць гэты допіс\?</string> + <string name="dialog_redraft_post_warning">Выдаліць і перапісаць гэты допіс\?</string> + <string name="dialog_delete_conversation_warning">Выдаліць гэтую размову\?</string> + <string name="mute_domain_warning">Вы ўпэўнены, што жадаеце заблакаваць усё з %1$s\? Вы не пабачыце допісы з гэтага дамена ва ўсіх агульнадаступных стужках або ў сваіх апавяшчэннях. Вашыя падпісчыкі з гэтага дамену будуць выдаленыя.</string> + <string name="mute_domain_warning_dialog_ok">Схаваць дамен цалкам</string> + <string name="dialog_block_warning">Заблакаваць @%1$s\?</string> + <string name="dialog_mute_warning">Ігнараваць %1$s\?</string> + <string name="dialog_mute_hide_notifications">Схаваць апавяшчэнні</string> + <string name="visibility_direct">Асабіста: Дасылаць толькі для згаданых карыстальнікаў</string> + <string name="pref_title_edit_notification_settings">Апавяшчэнні</string> + <string name="pref_title_notifications_enabled">Апавяшчэнні</string> + <string name="pref_title_notification_alerts">Апавяшчэнні</string> + <string name="pref_title_notification_alert_sound">Апавяшчаць з гукам</string> + <string name="pref_title_notification_filter_follow_requests">запыт на падпіску</string> + <string name="pref_title_notification_filter_reblogs">мае допісы пашыраны</string> + <string name="pref_title_notification_filter_favourites">мае допісы абраныя</string> + <string name="pref_title_notification_filter_poll">апытанні скончыліся</string> + <string name="pref_title_notification_filter_subscriptions">нехта, на каго я падпісаны(-ая), даслаў(-ла) новы допіс</string> + <string name="pref_title_notification_filter_sign_ups">нехта зарэгістраваўся</string> + <string name="pref_title_notification_filter_updates">допіс, з якім я ўзаемадзейнічаў(-ла), зменены</string> + <string name="pref_title_notification_filter_reports">з\'явілася новая скарга</string> + <string name="pref_title_appearance_settings">Выгляд</string> + <string name="pref_title_app_theme">Тэма</string> + <string name="pref_title_timelines">Стужкі</string> + <string name="pref_title_show_boosts">Паказаць пашырэнні</string> + <string name="pref_title_http_proxy_port">HTTP проксі порт</string> + <string name="pref_title_http_proxy_port_message">Порт павінен быць паміж %1$d і %2$d</string> + <string name="pref_default_post_privacy">Прыватнасць допісаў па змаўчанні</string> + <string name="pref_default_post_language">Мова допісаў па змаўчанні</string> + <string name="pref_default_media_sensitivity">Лічыць усе медыя далікатнымі</string> + <string name="pref_post_text_size">Памер шрыфту допісу</string> + <string name="post_text_size_smallest">Найменшы</string> + <string name="notification_mention_name">Новыя згадкі</string> + <string name="notification_mention_descriptions">Апавяшчаць аб новых згадках</string> + <string name="notification_follow_name">Новыя падпісчыкі</string> + <string name="notification_follow_description">Апавяшчаць аб новых падпісчыках</string> + <string name="notification_favourite_description">Паведамляць, калі вашыя допісы ўпадабаюць</string> + <string name="notification_poll_name">Апытанні</string> + <string name="notification_poll_description">Апавяшчаць аб заканчэнні апытанняў</string> + <string name="notification_subscription_name">Новыя допісы</string> + <string name="notification_subscription_description">Апавяшчаць аб новых допісах карыстальнікаў, на якіх Вы падпісаны</string> + <string name="notification_mention_format">%1$s узгадаў(-ла) Вас</string> + <string name="notification_summary_medium">%1$s, %2$s ды %3$s</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s ды %4$d іншых</string> + <string name="notification_summary_small">%1$s ды %2$s</string> + <string name="about_title_activity">Пра праграму</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_powered_by_tusky">Зроблена Tusky</string> + <string name="about_project_site">Вэбсайт праекту: +\n https://tusky.app</string> + <string name="about_tusky_account">Профіль Tusky</string> + <string name="post_share_content">Падзяліцца зместам допісу</string> + <string name="post_share_link">Падзяліцца спасылкай на допіс</string> + <string name="post_media_images">Выявы</string> + <string name="post_media_video">Відэа</string> + <string name="post_media_audio">Аўдыё</string> + <string name="post_media_attachments">Далучэнні</string> + <string name="status_count_one_plus">1+</string> + <string name="status_created_at_now">зараз</string> + <string name="state_follow_requested">Запыт на падпіску</string> + <string name="abbreviated_in_years">праз %1$dг</string> + <string name="abbreviated_in_days">праз %1$dд</string> + <string name="abbreviated_in_hours">праз %1$dгад</string> + <string name="abbreviated_in_minutes">праз %1$dхв</string> + <string name="abbreviated_in_seconds">праз %1$dс</string> + <string name="abbreviated_years_ago">%1$dг</string> + <string name="abbreviated_days_ago">%1$dд</string> + <string name="abbreviated_hours_ago">%1$dгад</string> + <string name="abbreviated_minutes_ago">%1$dхв</string> + <string name="abbreviated_seconds_ago">%1$dс</string> + <string name="title_media">Медыя</string> + <string name="pref_title_alway_open_spoiler">Заўсёды разгортваць допіс з далікатным зместам</string> + <string name="replying_to">Адказ для @%1$s</string> + <string name="pref_title_public_filter_keywords">Публічныя стужкі</string> + <string name="pref_title_thread_filter_keywords">Размовы</string> + <string name="load_more_placeholder_text">загрузіць больш</string> + <string name="about_tusky_license">Tusky — свабодная праграма з адкрытым зыходным кодам. Зроблена пад GNU General Public License Version 3. Вы можаце паглядзець ліцэнзію тут: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="about_bug_feature_request_site">Справаздачы аб памылках і пажаданні: +\n https://github.com/tuskyapp/Tusky/issues</string> + <string name="filter_addition_title">Дадаць фільтр</string> + <string name="follows_you">Вашы падпісчыкі</string> + <string name="pref_title_alway_show_sensitive_media">Заўсёды паказваць далікатны змест</string> + <string name="filter_edit_title">Рэдагаваць фільтр</string> + <string name="filter_dialog_remove_button">Выдаліць</string> + <string name="filter_dialog_update_button">Абнавіць</string> + <string name="filter_dialog_whole_word">Цэлае слова</string> + <string name="filter_dialog_whole_word_description">Калі ключавое слова ці фраза складаецца толькі з літар і лічбаў, то будзе ўлічвацца толькі ўсё слова</string> + <string name="title_edits">Рэдагаванні</string> + <string name="failed_to_add_to_list">Не атрымалася дадаць уліковы запіс у спіс</string> + <string name="action_add_or_remove_from_list">Дадаць ці выдаліць са спіса</string> + <string name="license_apache_2">Выкарыстоўваецца ліцэнзія Apache License (копія знізу)</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="action_add_to_list">Дадаць уліковы запіс у спіс</string> + <plurals name="hint_describe_for_visually_impaired"> + <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="compose_save_draft_loses_media">Захаваць чарнавік\? (Далучэнні будуць запампаваныя зноў, калі Вы аднавіце чарнавік.)</string> + <string name="performing_lookup_title">Выконваю пошук…</string> + <string name="caption_notoemoji">Бягучы набор эмодзі ад Google</string> + <string name="caption_twemoji">Тыповы набор эмодзі Mastodon</string> + <string name="download_failed">Не атрымалася спампаваць</string> + <string name="unreblog_private">Скасаваць пашырэнне</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="pin_action">Замацаваць</string> + <string name="unpin_action">Адмацаваць</string> + <string name="failed_to_pin">Не атрымалася замацаваць</string> + <string name="failed_to_unpin">Не атрымалася адмацаваць</string> + <string name="conversation_more_recipients">%1$s, %2$s і яшчэ %3$d</string> + <string name="action_discard">Скасаваць змены</string> + <string name="action_continue_edit">Працягнуць рэдагаванне</string> + <string name="hint_search_people_list">Пошук сярод вашых падпісак</string> + <string name="add_account_name">Дадаць уліковы запіс</string> + <string name="action_remove_from_list">Выдаліць уліковы запіс са спіса</string> + <string name="compose_active_account_description">Публікаваць як %1$s</string> + <string name="set_focus_description">Націсніце або перацягніце кола каб абраць пункт фокусу, які будзе заўсёды бачны на паменшаных выявах.</string> + <string name="action_set_caption">Задаць подпіс</string> + <string name="action_set_focus">Задаць пункт фокуса</string> + <string name="action_edit_image">Рэдагаваць выяву</string> + <string name="failed_to_remove_from_list">Не атрымалася выдаліць уліковы запіс са спіса</string> + <string name="send_post_notification_channel_name">Дасыланне допісаў</string> + <string name="later">Пазней</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> абраны</item> + <item quantity="few"><b>%1$s</b> абраныя</item> + <item quantity="many"><b>%1$s</b> абраных</item> + <item quantity="other"><b>%1$s</b> абраных</item> + </plurals> + <string name="title_reblogged_by">Пашырылі</string> + <string name="profile_metadata_label">Метазвесткі профілю</string> + <string name="title_favourited_by">Упадабалі</string> + <string name="add_account_description">Дадаць уліковы запіс Mastodon</string> + <string name="action_lists">Спісы</string> + <string name="title_lists">Спісы</string> + <string name="error_create_list">Не атрымалася стварыць спіс</string> + <string name="error_rename_list">Не атрымалася перайменаваць спіс</string> + <string name="error_delete_list">Не атрымалася выдаліць спіс</string> + <string name="action_create_list">Стварыць спіс</string> + <string name="action_rename_list">Перайменаваць спіс</string> + <string name="action_delete_list">Выдаліць спіс</string> + <string name="filter_add_description">Фільтраваць фразу</string> + <string name="action_remove">Выдаліць</string> + <string name="lock_account_label">Заблакаваць уліковы запіс</string> + <string name="compose_save_draft">Захаваць чарнавік\?</string> + <string name="send_post_notification_title">Дасыланне допісу…</string> + <string name="send_post_notification_error_title">Памылка пры дасыланні допісу</string> + <string name="lock_account_label_description">Вам прыйдзецца ўручную зацвярджаць падпісчыкаў</string> + <string name="action_compose_shortcut">Напісаць</string> + <string name="send_post_notification_cancel_title">Дасыланне скасаванае</string> + <string name="send_post_notification_saved_content">Копія допісу захавана ў Вашых чарнавіках</string> + <string name="error_no_custom_emojis">Ваш сервер %1$s не мае ўласных эмодзі</string> + <string name="emoji_style">Стыль эмодзі</string> + <string name="system_default">Сістэмнае значэнне па змаўчанні</string> + <string name="caption_systememoji">Тыповы набор эмодзі прылады</string> + <string name="caption_blobmoji">Эмодзі Blob з Android 4.4–7.1</string> + <string name="download_fonts">Спачатку трэба спампаваць гэтыя наборы эмодзі</string> + <string name="profile_metadata_add">дадаць звесткі</string> + <string name="profile_metadata_label_label">Пазнака</string> + <string name="profile_metadata_content_label">Змест</string> + <string name="pref_title_absolute_time">Паказваць абсалютны час</string> + <string name="label_remote_account">Інфармацыя ніжэй можа не паказваць поўны профіль карыстальніка. Націсніце, каб адкрыць поўны профіль у браўзеры.</string> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> пашыраны</item> + <item quantity="few"><b>%1$s</b> пашырана</item> + <item quantity="many"><b>%1$s</b> пашыраных</item> + <item quantity="other"><b>%1$s</b> пашыраных</item> + </plurals> + <string name="compose_unsaved_changes">У Вас засталіся незахаваныя змены.</string> + <string name="expand_collapse_all_posts">Разгарнуць/згарнуць допісы</string> + <string name="action_open_post">Адкрыць допіс</string> + <string name="restart_required">Патрэбна перазапусціць праграму</string> + <string name="restart_emoji">Патрэбна перазапусціць Tusky, каб прымяніць гэтыя змены</string> + <string name="profile_badge_bot_text">Бот</string> + <string name="account_moved_description">%1$s пераехаў(-ла) да:</string> + <string name="restart">Перазапусціць</string> + <string name="reblog_private">Пашырыць для пачатковай аўдыторыі</string> + <string name="license_description">Tusky змяшчае код і рэсурсы з наступных праграм з адкрытым зыходным кодам:</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s і %2$s</string> + <string name="description_post_media">Медыя: %1$s</string> + <string name="description_post_cw">Папярэджанне пра змесціва: %1$s</string> + <string name="description_post_media_no_description_placeholder">Няма апісання</string> + <string name="description_visibility_private">Падпісчыкі</string> + <string name="description_visibility_direct">Асабіста</string> + <string name="description_visibility_unlisted">Не ў стужках</string> + <string name="hashtags">Хэштэгі</string> + <string name="hint_list_name">Назва спіса</string> + <string name="add_hashtag_title">Дадаць хэштэг</string> + <string name="description_post_reblogged">Пашырана</string> + <string name="description_poll">Апытанне з варыянтамі: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="edit_hashtag_hint">Хэштэг без #</string> + <string name="description_post_edited">Зменена</string> + <string name="description_post_bookmarked">Дададзена ў закладкі</string> + <string name="description_post_favourited">Упадабана</string> + <string name="description_visibility_public">Публічны</string> + <string name="description_post_language">Мова допісу</string> + <string name="select_list_title">Выбраць спіс</string> + <string name="list">Спіс</string> + <string name="notifications_clear">Ачысціць</string> + <string name="notifications_apply_filter">Фільтр</string> + <string name="duration_365_days">365 дзён</string> + <string name="duration_no_change">(Без змен)</string> + <string name="compose_shortcut_long_label">Стварыць допіс</string> + <string name="compose_shortcut_short_label">Стварыць</string> + <string name="notification_clear_text">Вы ўпэўнены, што жадаеце выдаліць усе апавяшчэнні\?</string> + <string name="compose_preview_image_description">Дзеянні для выявы %1$s</string> + <string name="button_done">Гатова</string> + <string name="report_sent_success">Скарга на @%1$s адпраўлена</string> + <string name="hint_additional_info">Дадатковыя каментары</string> + <string name="title_accounts">Уліковыя запісы</string> + <string name="failed_search">Пошук не атрымаўся</string> + <string name="create_poll_title">Апытанне</string> + <string name="duration_1_day">1 дзень</string> + <string name="duration_3_days">3 дні</string> + <string name="button_continue">Працягнуць</string> + <plurals name="poll_timespan_minutes"> + <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="report_remote_instance">Пераслаць да %1$s</string> + <string name="pref_title_enable_swipe_for_tabs">Уключыць пераключэнне паміж укладкамі жэстам чыркання</string> + <plurals name="poll_timespan_seconds"> + <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> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s голас</item> + <item quantity="few">%1$s галасы</item> + <item quantity="many">%1$s галасоў</item> + <item quantity="other">%1$s галасоў</item> + </plurals> + <plurals name="poll_info_people"> + <item quantity="one">%1$s чалавек</item> + <item quantity="few">%1$s чалавекі</item> + <item quantity="many">%1$s людзей</item> + <item quantity="other">%1$s людзей</item> + </plurals> + <string name="label_duration">Працягласць</string> + <string name="button_back">Назад</string> + <string name="failed_report">Не атрымалася адправіць скаргу</string> + <string name="failed_fetch_posts">Не атрымалася дастаць допісы</string> + <string name="report_description_1">Скарга будзе даслана да мадэратара Вашага сервера. Ніжэй можна дадаць тлумачэнне, чаму Вы скардзіцеся на гэты ўліковы запіс:</string> + <string name="report_description_remote_instance">Гэты ўліковы запіс з іншага сервера. Даслаць ананімную копію скаргі туды таксама\?</string> + <string name="duration_indefinite">Бясконца</string> + <string name="duration_5_min">5 хвілін</string> + <string name="duration_30_min">30 хвілін</string> + <string name="action_share_account_link">Падзяліцца спасылкай на ўліковы запіс</string> + <string name="action_share_account_username">Падзяліцца імем уліковага запісу карыстальніка</string> + <string name="send_account_link_to">Падзяліцца URL уліковага запісу праз…</string> + <string name="send_account_username_to">Падзяліцца імем карыстальніка ўліковага запісу праз…</string> + <string name="account_username_copied">Імя карыстальніка скапіявана</string> + <string name="filter_apply">Прымяніць</string> + <string name="poll_info_format"> <!-- 15 галасоў • 1 гадзіна засталася --> %1$s • %2$s</string> + <string name="poll_info_time_absolute">завяршыцца ў %1$s</string> + <plurals name="poll_timespan_hours"> + <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="duration_1_hour">1 гадзіну</string> + <string name="poll_info_closed">скончаны</string> + <string name="poll_ended_voted">Апытанне, у якім Вы ўдзельнічалі, скончана</string> + <string name="poll_ended_created">Створанае Вамі апытанне скончана</string> + <plurals name="poll_timespan_days"> + <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="duration_6_hours">6 гадзін</string> + <string name="duration_7_days">7 дзён</string> + <string name="duration_14_days">14 дзён</string> + <string name="duration_30_days">30 дзён</string> + <string name="duration_60_days">60 дзён</string> + <string name="duration_90_days">90 дзён</string> + <string name="duration_180_days">180 дзён</string> + <string name="poll_vote">Галасаваць</string> + <string name="a11y_label_loading_thread">Спампоўваю стужку</string> + <string name="wellbeing_mode_notice">Некаторая інфармацыя, якая можа ўплываць на Ваш псіхічны стан, будзе скрыта. Гэта ўключае: +\n +\n- Апавяшчэнні аб Упадабаннях/Пашырэннях/Падпісках +\n- Колькасць Упадабанняў/Пашырэнняў у допісах +\n- Статыстыка ў профілях пра Падпісчыкаў/Допісы +\n +\nНа Push-паведамленні гэта не ўплывае, але Вы можаце праглядзець налады апавяшчэнняў уручную.</string> + <string name="dialog_push_notification_migration">Каб выкарыстоўваць push-апавяшчэнні праз UnifiedPush, Tusky патрэбен дазвол, каб падпісацца на апавяшчэнні з Вашага сервера Mastodon. Для гэтага патрэбна выйсці і зайсці ва ўліковы запіс зноў, каб аднавіць вобласці дазволу OAuth, дадзеныя Tusky. Выкарыстоўванне магчымасці паўторнага ўваходу тут ці ў наладах уліковага запісу захоўвае ўсе Вашыя лакальныя чарнавікі і кэш.</string> + <string name="poll_allow_multiple_choices">Некалькі варыянтаў</string> + <string name="add_poll_choice">Дадаць варыянт</string> + <string name="edit_poll">Змяніць</string> + <string name="post_lookup_error_format">Памылка пошуку допісу %1$s</string> + <string name="account_date_joined">Далучыўся(-лася) %1$s</string> + <string name="account_note_saved">Захавана!</string> + <string name="dialog_push_notification_migration_other_accounts">Вы паўторна зайшлі ў бягучы ўліковы запіс, каб дазволіць Tusky падпісацца на push-апавяшчэнні. Але ў Вас яшчэ засталіся ўліковыя запісы, якія не мігрыравалі такім чынам. Пераключыцеся на іх і зайдзіце паўторна, каб уключыць падтрымку апавяшчэнняў праз UnifiedPush.</string> + <string name="status_edit_info">%1$s адрэдагаваў(-ла)</string> + <string name="status_created_info">%1$s стварыў(-ла)</string> + <string name="pref_title_hide_top_toolbar">Схаваць загаловак верхняй панэлі інструментаў</string> + <string name="account_note_hint">Ваша асабістая нататка пра гэты ўліковы запіс</string> + <string name="pref_title_confirm_favourites">Патрабаваць пацвярджэнне перад упадабаннем</string> + <string name="pref_title_confirm_reblogs">Патрабаваць пацвярджэнне перад пашырэннем</string> + <string name="pref_title_wellbeing_mode">Дабрабыт</string> + <string name="drafts_failed_loading_reply">Немагчыма атрымаць інфармацыю пра адказ</string> + <string name="draft_deleted">Чарнавік выдалены</string> + <string name="report_category_spam">Спам</string> + <string name="report_category_other">Іншае</string> + <string name="action_unfollow_hashtag_format">Не сачыць за #%1$s\?</string> + <string name="action_subscribe_account">Падпісацца</string> + <string name="action_unsubscribe_account">Адпісацца</string> + <string name="tusky_compose_post_quicksetting_label">Стварыць допіс</string> + <string name="saving_draft">Захоўваю чарнавік…</string> + <string name="drafts_post_reply_removed">Допіс, для якога Вы стварылі чарнавік адказу, выдалены</string> + <string name="follow_requests_info">Нягледзячы на тое што Ваш уліковы запіс не закрыты, кіраўнікі %1$s вырашылі, што Вы, магчыма, пажадаеце праглядзець запыты на падпіску ад гэтых уліковых запісаў уручную.</string> + <string name="no_lists">У Вас няма спісаў.</string> + <string name="no_drafts">У Вас няма чарнавікоў.</string> + <string name="no_scheduled_posts">У Вас няма запланаваных допісаў.</string> + <string name="no_announcements">Няма аб\'яў.</string> + <string name="pref_title_show_self_username">Паказваць імя карыстальніка на панэлях інструментаў</string> + <string name="pref_title_show_cards_in_timelines">Паказваць папярэдні прагляд спасылак у стужках</string> + <string name="review_notifications">Праглядзець апавяшчэнні</string> + <plurals name="error_upload_max_media_reached"> + <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="dialog_delete_list_warning">Вы ўпэўнены, што жадаеце выдаліць спіс %1$s\?</string> + <string name="limit_notifications">Абмежаванне апавяшчэнняў у стужцы</string> + <string name="wellbeing_hide_stats_posts">Схаваць колькасную статыстыку допісаў</string> + <string name="wellbeing_hide_stats_profile">Схаваць колькасную статыстыку профіляў</string> + <string name="report_category_violation">Парушэнне правіла</string> + <string name="instance_rule_title">Правілы %1$s</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="delete_scheduled_post_warning">Выдаліць гэты запланаваны допіс\?</string> + <string name="instance_rule_info">Увайшоўшы, Вы пагаджаецеся з правіламі %1$s.</string> + <string name="poll_new_choice_hint">Варыянт %1$d</string> + <string name="drafts_post_failed_to_send">Не атрымалася даслаць гэты допіс!</string> + <string name="tips_push_notification_migration">Увайдзіце зноў на ўсіх уліковых запісах, каб push-апавяшчэнні запрацавалі.</string> + <string name="mute_notifications_switch">Бязгучныя апавяшчэнні</string> + <string name="warning_scheduling_interval">Найменшы час планавання ў Mastodon складае 5 хвілін.</string> + <string name="pref_reading_order_oldest_first">Спачатку старэйшыя</string> + <string name="pref_title_reading_order">Кірунак чытання</string> + <string name="pref_summary_http_proxy_disabled">Адключана</string> + <string name="pref_summary_http_proxy_invalid"><нядзейсны></string> + <string name="pref_summary_http_proxy_missing"><не задана></string> + <string name="pref_reading_order_newest_first">Спачатку навейшыя</string> + <string name="dialog_follow_hashtag_title">Падпісацца на хэштэг</string> + <string name="dialog_follow_hashtag_hint">#hashtag</string> + <string name="action_post_failed">Не атрымалася запампаваць</string> + <string name="action_post_failed_detail">Не атрымалася запампаваць ваш допіс і ён быў захаваны ў чарнавік. +\n +\nАльбо не атрымалася злучыцца з серверам ці ён адхіліў допіс.</string> + <string name="action_post_failed_detail_plural">Не атрымалася запампаваць вашы допісы і яны былі захаваны ў чарнавікі. +\n +\nАльбо не атрымалася злучыцца з серверам ці ён адхіліў допісы.</string> + <string name="action_post_failed_show_drafts">Паказаць чарнавікі</string> + <string name="action_post_failed_do_nothing">Адмяніць</string> + <string name="description_login">Працуе амаль заўсёды. Даныя не ўцякаюць у іншыя праграмы.</string> + <string name="action_browser_login">Увайсці з браўзера</string> + <string name="notification_unknown_name">Невядома</string> + <string name="status_filtered_show_anyway">Усё роўна паказаць</string> + <string name="status_filter_placeholder_label_format">Адфільтрована: %1$s</string> + <string name="pref_title_account_filter_keywords">Профілі</string> + <string name="title_public_trending_hashtags">Папулярныя хэштэгі</string> + <string name="description_browser_login">Можа падтрымліваць дадатковыя метады праверкі сапраўднасці, але для гэтага патрэбны адпаведны браузер.</string> + <string name="post_media_image">Відарыс</string> + <string name="action_refresh">Абнавіць</string> + <string name="select_list_manage">Кіраванне спісамі</string> + <string name="total_usage">Усяго выкарыстана</string> + <string name="total_accounts">Усяго ўліковых запісаў</string> + <string name="accessibility_talking_about_tag">%1$d людзей кажуць пра хэштэг %2$s</string> + <string name="pref_title_show_stat_inline">Паказваць статыстыку допісаў у стужцы</string> + <string name="ui_error_reblog">Не атрымалася пашырыць допіс: %1$s</string> + <string name="hint_filter_title">Мой фільтр</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="ui_error_bookmark">Не атрымалася дадаць допіс да закладак: %1$s</string> + <string name="ui_error_accept_follow_request">Не атрымалася пацвердзіць запыт на падпіску: %1$s</string> + <string name="ui_success_accepted_follow_request">Запыт на падпіску пацверджаны</string> + <string name="ui_success_rejected_follow_request">Запыт на падпіску заблакіраваны</string> + <string name="ui_error_clear_notifications">Не атрымалася ачысціць апавяшчэнні: %1$s</string> + <string name="ui_error_unknown">невядомая прычына</string> + <string name="label_filter_title">Загаловак</string> + <string name="filter_action_hide">Схаваць</string> + <string name="filter_description_hide">Схаваць поўнасцю</string> + <string name="label_filter_context">Фільтраваць кантэкст</string> + <string name="label_filter_keywords">Ключавыя словы альбо фразы для фільтравання</string> + <string name="action_add">Дадаць</string> + <string name="filter_keyword_display_format">%1$s (цэлае слова)</string> + <string name="filter_keyword_addition_title">Дадаць ключавое слова</string> + <string name="filter_edit_keyword_title">Змяніць ключавое слова</string> + <string name="reply_sending_long">Ваш адказ адасланы.</string> + <string name="reply_sending">Адсылаю…</string> + <string name="label_image">Рысунак</string> + <string name="action_translate">Перакласці</string> + <string name="action_show_original">Паказаць арыгінал</string> + <string name="title_public_trending_statuses">Папулярныя допісы</string> + <string name="error_blocking_domain">Не атрымалася заблакіраваць %1$s: %2$s</string> + <string name="error_unblocking_domain">Не атрымалася разблакіраваць %1$s: %2$s</string> + <string name="pref_title_per_timeline_preferences">Налады па стужкам</string> + <string name="pref_title_show_self_boosts">Паказаць самацытаванне</string> + <string name="pref_title_show_self_boosts_description">Нехта цытуе ўласныя допісы</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-ber/strings.xml b/app/src/main/res/values-ber/strings.xml new file mode 100644 index 0000000..c754aef --- /dev/null +++ b/app/src/main/res/values-ber/strings.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="title_tab_preferences">ⵉⵛⵛⴰⵔⴻⵏ</string> + <string name="action_view_account_preferences">ⵉⵖⴻⵡⵡⴰⵕⴻⵏ ⵏ ⵓⵎⵉⴸⴰⵏ</string> + <string name="action_view_preferences">ⵉⵖⴻⵡⵡⴰⵕⴻⵏ</string> + <string name="pref_title_proxy_settings">ⴰⵒⵕⵓⴽⵙⵉ</string> + <string name="action_view_blocks">ⵉⵎⵙⴻⵇⴷⴰⵛⴻⵏ ⵜⵙⵡⴰⵃⵍⴻⵎ</string> + <string name="action_view_favourites">ⵉⵙⵎⴻⵏⵢⵉⴼⴻⵏ</string> + <string name="action_view_profile">ⴰⵎⴻⵖⵏⵓ</string> + <string name="action_close">ⵎⴷⴻⵍ</string> + <string name="error_generic">ⵝⴻⵍⵍⴰ ⴷ ⵝⵓⵛⴹⴰ.</string> + <string name="title_lists">ⵝⴰⴲⴸⴰⵔⵉⵏ</string> + <string name="action_lists">ⵝⵉⴲⴸⴰⵔⵉⵏ</string> + <string name="about_title_activity">ⵖⴻⴼ</string> + <string name="action_reset_schedule">ⵡⴻⵏⵏⴻⵣ ⵝⵉⴽⴻⵍⵜ ⵏⵏⵉⴸⴻⵏ</string> + <string name="action_search">ⵏⴰⴸⵉ</string> + <string name="action_edit_profile">ⵣⵔⴻⴳ ⴰⵎⴰⵖⵏⵓ</string> + <string name="action_logout">ⴼⴼⴻⵖ</string> + <string name="title_drafts">ⵉⵔⴻⵡⵡⴰⵢⴻⵏ</string> + <string name="title_favourites">ⵉⵙⵎⴻⵏⵢⵉⴼⴻⵏ</string> + <string name="button_back">ⵓⵖⴰⵍ</string> + <string name="button_continue">ⴽⴻⵎⵎⴻⵍ</string> + <string name="filter_dialog_update_button">ⵍⵇⴻⵎ</string> + <string name="filter_dialog_remove_button">ⴽⴽⴻⵙ</string> + <string name="action_send_public">ⵊⴻⵡⵡⴻⵇ!</string> + <string name="action_send">ⵊⴻⵡⵡⴻⵇ</string> + <string name="action_login">ⵇⵇⴻⵏ ⵖⴻⵔ ⵎⴰⵚⵟⵓⴷⵓⵏ</string> + <string name="link_whats_an_instance">ⴸ ⴰⵛⵓ ⵓⴸ ⵜⵜⵓⵎⵎⴰⵏⵜ\?</string> + <string name="notification_favourite_name">ⵉⵙⵎⴻⵏⵢⵉⴼⴻⵏ</string> + <string name="action_remove">ⴽⴽⴻⵙ</string> + <string name="action_access_drafts">ⵉⵔⴻⵡⵡⴰⵢⴻⵏ</string> + <string name="title_blocks">ⵉⵎⵙⴻⵇⴷⴰⵛⴻⵏ ⵜⵙⵡⴰⵃⵍⴻⵎ</string> + <string name="pref_title_post_tabs">ⵉⵛⵛⴰⵔⴻⵏ</string> + <string name="hint_domain">ⴰⵏⵜⴰ ⵝⵓⵎⵎⴰⵏⵜ\?</string> + <string name="add_account_description">ⵔⵏⵓ ⵢⵉⵡⴻⵏ ⵏ ⵓⵎⵉⴹⴰⵏ ⴰⵎⴰⵢⵏⵓⵝ ⵏ ⵎⴰⵚⵟⵓⴷⵓⵏ</string> + <string name="add_account_name">ⵔⵏⵓ ⴰⵎⵉⴹⴰⵏ</string> + <string name="hint_compose">ⴸⴰⵛⵓ ⵉⴳⴻⵍⵍⴰⵏ ⴸ ⴰⵎⴰⵢⵏⵓⵝ\?</string> + <string name="action_schedule_post">ⵙⵖⵉⵡⴻⵙ ⵝⵉⵊⴻⵡⵡⵉⵇⵝⴰ</string> + <string name="action_access_scheduled_posts">ⵝⵉⵊⴻⵡⵡⵉⵇⵉⵏ ⵢⴻⵜⵜⵖⴰⵙⵖⴰⵡⵙⴻⵏ</string> + <string name="title_scheduled_posts">ⵝⵉⵊⴻⵡⵡⵉⵇⵉⵏ ⵢⴻⵜⵜⵖⴰⵙⵖⴰⵡⵙⴻⵏ</string> + <string name="title_bookmarks">ⵝⵉⵛⵔⴰⴹ</string> + <string name="action_view_bookmarks">ⵝⵉⵛⵔⴰⴹ</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-bg/strings.xml b/app/src/main/res/values-bg/strings.xml new file mode 100644 index 0000000..bc7fc1e --- /dev/null +++ b/app/src/main/res/values-bg/strings.xml @@ -0,0 +1,597 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="title_view_thread">Нишка</string> + <string name="drafts_post_reply_removed">Публикацията, на която сте изготвили отговор, е премахната</string> + <string name="duration_1_hour">1 час</string> + <string name="duration_30_min">30 минути</string> + <string name="duration_5_min">5 минути</string> + <string name="duration_indefinite">Неопределено</string> + <string name="label_duration">Продължителност</string> + <string name="create_poll_title">Анкета</string> + <string name="pref_title_enable_swipe_for_tabs">Активиране на плъзгащия жест за превключване между раздели</string> + <string name="failed_search">Търсенето бе неуспешно</string> + <string name="title_accounts">Акаунти</string> + <string name="report_description_remote_instance">Акаунтът е от друг сървър. Да изпратите ли и там анонимно копие на доклада\?</string> + <string name="report_description_1">Докладът ще бъде изпратен на модератора на вашия сървър. Можете да предоставите обяснение защо докладвате този акаунт по-долу:</string> + <string name="failed_fetch_posts">Извличането на публикации бе неуспешно</string> + <string name="failed_report">Докладването бе неуспешно</string> + <string name="report_remote_instance">Препращане към %1$s</string> + <string name="hint_additional_info">Допълнителни коментари</string> + <string name="report_sent_success">Успешно докладване на @%1$s</string> + <string name="button_done">Готово</string> + <string name="button_back">Назад</string> + <string name="button_continue">Продължаване</string> + <plurals name="poll_timespan_seconds"> + <item quantity="one">Остава %1$d секунда</item> + <item quantity="other">Остават %1$d секунди</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">Остава %1$d минута</item> + <item quantity="other">Остават %1$d минути</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">Остава %1$d час</item> + <item quantity="other">Остават %1$d часа</item> + </plurals> + <plurals name="poll_timespan_days"> + <item quantity="one">Остава %1$d ден</item> + <item quantity="other">Остават %1$d дни</item> + </plurals> + <string name="poll_ended_created">Анкета, която създадохте, приключи</string> + <string name="poll_ended_voted">Анкета, в която сте гласували, приключи</string> + <string name="poll_vote">Гласуване</string> + <string name="poll_info_closed">затворено</string> + <string name="poll_info_time_absolute">завършва в %1$s</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s човек</item> + <item quantity="other">%1$s човека</item> + </plurals> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s глас</item> + <item quantity="other">%1$s гласа</item> + </plurals> + <string name="poll_info_format"> \u0020<!-- 15 votes • 1 hour left --> \u0020%1$s • %2$s</string> + <string name="compose_preview_image_description">Действия за изображение %1$s</string> + <string name="notification_clear_text">Сигурни ли сте, че искате да изчистите окончателно всичките си известия\?</string> + <string name="compose_shortcut_short_label">Композиране</string> + <string name="compose_shortcut_long_label">Композиране на публикация</string> + <string name="filter_apply">Прилагане</string> + <string name="notifications_apply_filter">Филтриране</string> + <string name="notifications_clear">Изчистване</string> + <string name="list">Списък</string> + <string name="select_list_title">Избиране на списък</string> + <string name="hashtags">Хаштагове</string> + <string name="edit_hashtag_hint">Хаштаг без #</string> + <string name="add_hashtag_title">Добавяне на хаштаг</string> + <string name="hint_list_name">Име на списък</string> + <string name="description_poll">Анкета с избори: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="description_visibility_direct">Директно</string> + <string name="description_visibility_private">Последователи</string> + <string name="description_visibility_public">Публично</string> + <string name="description_post_bookmarked">Отметнато</string> + <string name="description_post_favourited">Поставено в любими</string> + <string name="description_post_reblogged">Реблог</string> + <string name="description_post_media_no_description_placeholder">Няма описание</string> + <string name="description_post_cw">Предупреждение за съдържание: %1$s</string> + <string name="description_post_media">Мултимедия: %1$s</string> + <string name="conversation_more_recipients">%1$s, %2$s и %3$d други</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s и %2$s</string> + <string name="title_favourited_by">Поставено в любими от</string> + <string name="title_reblogged_by">Споделено от</string> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> Споделяне</item> + <item quantity="other"><b>%1$s</b> Споделяния</item> + </plurals> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Любимо</item> + <item quantity="other"><b>%1$s</b> Любими</item> + </plurals> + <string name="pin_action">Закачане</string> + <string name="unpin_action">Разкачане</string> + <string name="label_remote_account">Информацията по-долу може да отразява непълно потребителския профил. Натиснете, за да отворите пълен профил в браузъра.</string> + <string name="pref_title_absolute_time">Използване на абсолютно време</string> + <string name="profile_metadata_content_label">Съдържание</string> + <string name="profile_metadata_label_label">Етикет</string> + <string name="profile_metadata_add">добавяне на данни</string> + <string name="profile_metadata_label">Профилни метаданни</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_apache_2">Лицензиран под лиценза Apache (копие по-долу)</string> + <string name="license_description">Tusky съдържа код и активи от следните проекти с отворен код:</string> + <string name="unreblog_private">Отсподеляне</string> + <string name="reblog_private">Споделяне с оригиналната аудитория</string> + <string name="account_moved_description">%1$s се премести в:</string> + <string name="profile_badge_bot_text">Бот</string> + <string name="download_failed">Изтеглянето се провали</string> + <string name="caption_notoemoji">Текущият набор от емоджита на Google</string> + <string name="download_fonts">Първо ще трябва да изтеглите тези емоджи комплекти</string> + <string name="caption_twemoji">Стандартният емоджи комплект на Mastodon</string> + <string name="caption_blobmoji">Blob емоджитата, известни от Android 4.4–7.1</string> + <string name="caption_systememoji">Емоджи комплектът по подразбиране в устройство ви</string> + <string name="restart">Рестартиране</string> + <string name="later">По-късно</string> + <string name="restart_emoji">Ще трябва да рестартирате Tusky, за да приложите тези промени</string> + <string name="restart_required">Изисква се рестартиране на приложението</string> + <string name="action_open_post">Отваряне на публикация</string> + <string name="expand_collapse_all_posts">Разгъване/сгъване на всички публикации</string> + <string name="performing_lookup_title">Извършва се търсене…</string> + <string name="system_default">По подразбиране от системата</string> + <string name="emoji_style">Стил на емоджи</string> + <string name="error_no_custom_emojis">Инстанцията ви %1$s няма персонализирани емоджита</string> + <string name="action_compose_shortcut">Композиране</string> + <string name="send_post_notification_saved_content">Копие от публикацията е запазено във вашите чернови</string> + <string name="send_post_notification_cancel_title">Изпращането е отменено</string> + <string name="send_post_notification_channel_name">Изпращане на публикации</string> + <string name="send_post_notification_error_title">Грешка при изпращане на публикация</string> + <string name="send_post_notification_title">Изпращане на публикация…</string> + <string name="compose_save_draft">Запазване на чернова\?</string> + <string name="lock_account_label_description">Изисква ръчно одобряване на последователи</string> + <string name="lock_account_label">Заключване на акаунт</string> + <string name="action_remove">Премахване</string> + <string name="action_set_caption">Задаване на надпис</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Опишете за хора със зрителни увреждания (ограничение до %1$d знак)</item> + <item quantity="other">Опишете за хора със зрителни увреждания (ограничение до %1$d знака)</item> + </plurals> + <string name="compose_active_account_description">Публикуване като %1$s</string> + <string name="action_remove_from_list">Премахване на акаунт от списъка</string> + <string name="action_add_to_list">Добавяне на акаунт към списъка</string> + <string name="hint_search_people_list">Търсене на хора, които следвате</string> + <string name="action_delete_list">Изтриване на списъка</string> + <string name="action_rename_list">Обновяване на списъка</string> + <string name="action_create_list">Създаване на списък</string> + <string name="error_delete_list">Списъкът не можа да се изтрие</string> + <string name="error_create_list">Списъкът не можа да се създаде</string> + <string name="error_rename_list">Списъкът не можа да се обнови</string> + <string name="title_lists">Списъци</string> + <string name="action_lists">Списъци</string> + <string name="add_account_description">Добавяне на нов Mastodon акаунт</string> + <string name="add_account_name">Добавяне на акаунт</string> + <string name="filter_add_description">Фраза за филтриране</string> + <string name="filter_dialog_whole_word_description">Когато ключовата дума или фраза е само буквено-цифрова, тя ще бъде приложена само ако съответства на цялата дума</string> + <string name="filter_dialog_whole_word">Цяла дума</string> + <string name="filter_dialog_update_button">Обновяване</string> + <string name="filter_dialog_remove_button">Премахване</string> + <string name="filter_edit_title">Редакция на филтър</string> + <string name="filter_addition_title">Добавяне на филтър</string> + <string name="pref_title_thread_filter_keywords">Разговори</string> + <string name="pref_title_public_filter_keywords">Публични емисии</string> + <string name="load_more_placeholder_text">зареждане на още</string> + <string name="replying_to">Отговаряне на @%1$s</string> + <string name="title_media">Мултимедия</string> + <string name="pref_title_alway_open_spoiler">Винаги разгъване на публикации, маркирани с предупреждения за съдържание</string> + <string name="pref_title_alway_show_sensitive_media">Винаги показване на деликатно съдържание</string> + <string name="follows_you">Следва ви</string> + <string name="abbreviated_seconds_ago">%1$dс</string> + <string name="abbreviated_minutes_ago">%1$dм</string> + <string name="abbreviated_hours_ago">%1$dч</string> + <string name="abbreviated_days_ago">%1$dд</string> + <string name="abbreviated_years_ago">%1$dг</string> + <string name="abbreviated_in_seconds">след %1$dс</string> + <string name="abbreviated_in_minutes">след %1$dм</string> + <string name="abbreviated_in_hours">след %1$dч</string> + <string name="abbreviated_in_years">след %1$dг</string> + <string name="abbreviated_in_days">след %1$dд</string> + <string name="state_follow_requested">Заявено последване</string> + <string name="post_media_attachments">Прикачени файлове</string> + <string name="post_media_audio">Аудио</string> + <string name="post_media_video">Видео</string> + <string name="post_media_images">Изображения</string> + <string name="post_share_link">Споделяне на връзка към публикация</string> + <string name="post_share_content">Споделяне на съдържание на публикация</string> + <string name="about_tusky_account">Профилът на Tusky</string> + <string name="about_bug_feature_request_site">Доклади за грешки и заявки за функции: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_project_site">Уебсайт на проекта: https://tusky.app</string> + <string name="about_tusky_license">Tusky е свободен софтуер с отворен код. Той е лицензиран под Общият публичен лиценз на GNU Версия 3. Можете да видите лиценза тук: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="about_powered_by_tusky">Осъществено от Tusky</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_title_activity">Относно</string> + <string name="description_account_locked">Заключен акаунт</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d ново взаимодействие</item> + <item quantity="other">%1$d нови взаимодействия</item> + </plurals> + <string name="notification_summary_small">%1$s и %2$s</string> + <string name="notification_summary_medium">%1$s, %2$s, и %3$s</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s и %4$d други</string> + <string name="notification_mention_format">%1$s ви спомена</string> + <string name="notification_subscription_description">Известия, когато някой, за когото сте абонирани, публикува</string> + <string name="notification_subscription_name">Нови публикации</string> + <string name="notification_poll_description">Известия за приключили анкети</string> + <string name="notification_poll_name">Анкети</string> + <string name="notification_favourite_description">Известия, когато публикациите ви бъдат означени като любими</string> + <string name="notification_favourite_name">Любими</string> + <string name="notification_boost_description">Известия, когато публикациите ви се споделят</string> + <string name="post_text_size_smallest">Най-малък</string> + <string name="description_visibility_unlisted">Скрито</string> + <string name="pref_title_post_tabs">Начална емисия</string> + <string name="pref_title_post_filter">Филтриране на емисия</string> + <string name="pref_title_animate_custom_emojis">Анимиране на персонализирани емоджита</string> + <string name="pref_title_gradient_for_media">Показване на цветни градиенти за скрита мултимедия</string> + <string name="pref_title_animate_gif_avatars">Анимиране на GIF аватари</string> + <string name="pref_title_bot_overlay">Показване на индикатор за ботове</string> + <string name="pref_title_language">Език</string> + <string name="pref_title_custom_tabs">Използване на персонализирани раздели чрез Chrome</string> + <string name="pref_title_browser_settings">Браузър</string> + <string name="app_theme_system">Използване на системния дизайн</string> + <string name="app_theme_auto">Автоматично при залез</string> + <string name="app_theme_black">Черно</string> + <string name="app_theme_light">Светло</string> + <string name="app_them_dark">Тъмно</string> + <string name="pref_title_timeline_filters">Филтри</string> + <string name="pref_title_timelines">Емисии</string> + <string name="pref_title_app_theme">Тема на приложение</string> + <string name="pref_title_appearance_settings">Външен вид</string> + <string name="pref_title_notification_filter_subscriptions">някой, за когото съм абониран, публикува</string> + <string name="pref_title_notification_filter_poll">приключили анкети</string> + <string name="pref_title_notification_filter_favourites">публикациите ми са сложени в любими</string> + <string name="pref_title_notification_filter_reblogs">публикациите ми са споделени</string> + <string name="pref_title_notification_filter_follow_requests">заявка за последване</string> + <string name="pref_title_notification_filter_follows">последвани</string> + <string name="pref_title_notification_filter_mentions">споменати</string> + <string name="pref_title_notification_filters">Уведомете ме когато</string> + <string name="pref_title_notification_alert_light">Уведомяване със светлина</string> + <string name="pref_title_notification_alert_vibrate">Уведомяване с вибрация</string> + <string name="pref_title_notification_alert_sound">Уведомяване със звук</string> + <string name="pref_title_notification_alerts">Сигнали</string> + <string name="pref_title_notifications_enabled">Известия</string> + <string name="pref_title_edit_notification_settings">Известия</string> + <string name="visibility_direct">Директно: Публикуване само за споменатите потребители</string> + <string name="visibility_private">Само за последователи: Публикуване само за последователи</string> + <string name="visibility_public">Публично: Публикуване в публични емисии</string> + <string name="dialog_mute_hide_notifications">Скриване на известия</string> + <string name="dialog_mute_warning">Заглушаване на @%1$s\?</string> + <string name="dialog_block_warning">Блокиране на @%1$s?</string> + <string name="mute_domain_warning_dialog_ok">Скриване на целия домейн</string> + <string name="mute_domain_warning">Сигурни ли сте, че искате да блокирате всички от %1$s\? Няма да виждате съдържание от този домейн в нито една публична емисия или във вашите известия. Последователите ви от този домейн ще бъдат премахнати.</string> + <string name="dialog_redraft_post_warning">Да се изтрие и преработи ли тази публикация?</string> + <string name="dialog_delete_post_warning">Да се изтрие ли тази публикация?</string> + <string name="dialog_unfollow_warning">Отследване на този акаунт\?</string> + <string name="dialog_message_cancel_follow_request">Да се отмени ли заявката за последване?</string> + <string name="dialog_download_image">Изтегляне</string> + <string name="dialog_message_uploading_media">Качване…</string> + <string name="dialog_title_finishing_media_upload">Завършване на мултимедийно качване</string> + <string name="dialog_whats_an_instance">Тук може да се въведе адресът или домейнът на която и да е инстанция, като mastodon.social, icosahedron.website, social.tchncs.de и <a href="https://instances.social">други!</a> +\n +\nАко все още нямате акаунт, можете да въведете името на инстанцията, към която искате да се присъедините, и да създадете акаунт там. +\n +\nИнстанцията е единично място, където се хоства акаунтът ви, но можете лесно да комуникирате и да следвате хора в други инстанции, сякаш сте на същия сайт. +\n +\nПовече информация можете да намерите на <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="login_connection">Свързване…</string> + <string name="link_whats_an_instance">Какво е инстанция\?</string> + <string name="label_header">Заглавна част</string> + <string name="label_avatar">Аватар</string> + <string name="label_quick_reply">Отговор…</string> + <string name="search_no_results">Няма резултати</string> + <string name="hint_search">Търсене…</string> + <string name="hint_note">Био</string> + <string name="hint_display_name">Показвано име</string> + <string name="hint_content_warning">Предупреждение за съдържание</string> + <string name="hint_compose">Какво се случва\?</string> + <string name="hint_domain">Коя инстанция\?</string> + <string name="confirmation_domain_unmuted">%1$s е разкрит</string> + <string name="confirmation_unmuted">Потребителят е раззаглушен</string> + <string name="confirmation_unblocked">Потребителят е деблокиран</string> + <string name="confirmation_reported">Изпратено!</string> + <string name="send_media_to">Споделяне на мултимедия в…</string> + <string name="send_post_content_to">Споделяне на публикация в…</string> + <string name="send_post_link_to">Споделяне на URL адреса на публикацията в…</string> + <string name="downloading_media">Теглене на мултимедия</string> + <string name="download_media">Изтегляне на мултимедия</string> + <string name="action_share_as">Споделяне като …</string> + <string name="action_open_as">Отваряне като %1$s</string> + <string name="action_copy_link">Копиране на връзката</string> + <string name="download_image">Изтегляне на %1$s</string> + <string name="action_open_media_n">Отваряне на мултимедия #%1$d</string> + <string name="title_links_dialog">Връзки</string> + <string name="title_mentions_dialog">Споменавания</string> + <string name="title_hashtags_dialog">Хаштагове</string> + <string name="action_open_faved_by">Показване на любими</string> + <string name="action_open_reblogged_by">Показване на споделяния</string> + <string name="action_open_reblogger">Отваряне на споделилия автор</string> + <string name="action_hashtags">Хаштагове</string> + <string name="action_mentions">Споменавания</string> + <string name="action_links">Връзки</string> + <string name="action_add_tab">Добавяне на раздел</string> + <string name="action_reset_schedule">Нулиране</string> + <string name="action_schedule_post">Планиране на публикация</string> + <string name="action_emoji_keyboard">Емоджи клавиатура</string> + <string name="action_content_warning">Предупреждение за съдържание</string> + <string name="action_toggle_visibility">Видимост на публикация</string> + <string name="action_access_scheduled_posts">Планирани публикации</string> + <string name="action_access_drafts">Чернови</string> + <string name="action_search">Търсене</string> + <string name="action_reject">Отхвърляне</string> + <string name="action_accept">Приемане</string> + <string name="action_undo">Отмяна</string> + <string name="action_edit_own_profile">Редакция</string> + <string name="action_edit_profile">Редакция на профил</string> + <string name="action_save">Запазване</string> + <string name="action_open_drawer">Отваряне на чекмедже</string> + <string name="action_hide_media">Скриване на мултимедия</string> + <string name="action_mention">Споменаване</string> + <string name="action_unmute_conversation">Раззаглушаване на разговор</string> + <string name="action_mute_conversation">Заглушаване на разговор</string> + <string name="action_unmute_domain">Раззаглушаване на %1$s</string> + <string name="action_mute_domain">Заглушаване на %1$s</string> + <string name="action_unmute_desc">Раззаглушаване на %1$s</string> + <string name="action_unmute">Раззаглушаване</string> + <string name="action_mute">Заглушаване</string> + <string name="action_share">Споделяне</string> + <string name="action_photo_take">Снимане</string> + <string name="action_add_poll">Добавяне на анкета</string> + <string name="action_add_media">Добавяне на мултимедия</string> + <string name="action_open_in_web">Отваряне в браузъра</string> + <string name="action_view_media">Мултимедия</string> + <string name="action_view_follow_requests">Заявки за последване</string> + <string name="action_view_domain_mutes">Скрити домейни</string> + <string name="action_view_blocks">Блокирани потребители</string> + <string name="action_view_mutes">Заглушени потребители</string> + <string name="action_view_bookmarks">Отметки</string> + <string name="action_view_favourites">Любими</string> + <string name="action_view_account_preferences">Предпочитания за акаунта</string> + <string name="action_view_preferences">Предпочитания</string> + <string name="action_view_profile">Профил</string> + <string name="action_close">Затваряне</string> + <string name="action_retry">Повторен опит</string> + <string name="action_send_public">ПУБЛИКУВАНЕ!</string> + <string name="action_send">ИЗПРАЩАНЕ</string> + <string name="action_delete_and_redraft">Изтриване и преработване</string> + <string name="action_delete">Изтриване</string> + <string name="action_edit">Редакция</string> + <string name="action_report">Докладване</string> + <string name="action_show_reblogs">Показване на споделяния</string> + <string name="action_hide_reblogs">Скриване на споделяния</string> + <string name="action_unblock">Деблокиране</string> + <string name="action_block">Блокиране</string> + <string name="action_unfollow">Отследване</string> + <string name="action_follow">Последване</string> + <string name="action_logout_confirm">Сигурни ли сте, че искате да излезете от %1$s\? Това ще изтрие всички локални данни на акаунта, включително чернови и предпочитания.</string> + <string name="action_logout">Излизане</string> + <string name="action_login">Вписване с Tusky</string> + <string name="action_compose">Композиране</string> + <string name="action_more">Още</string> + <string name="action_unfavourite">Премахване от любими</string> + <string name="action_bookmark">Отмятане</string> + <string name="action_favourite">Поставяне в любими</string> + <string name="action_unreblog">Премахване на споделяне</string> + <string name="action_reblog">Споделяне</string> + <string name="action_reply">Отговор</string> + <string name="action_quick_reply">Бърз отговор</string> + <string name="report_comment_hint">Допълнителни коментари\?</string> + <string name="report_username_format">Докладване на @%1$s</string> + <string name="notification_subscription_format">%1$s току-що публикува</string> + <string name="notification_follow_request_format">%1$s поиска да ви последва</string> + <string name="notification_follow_format">%1$s ви последва</string> + <string name="notification_favourite_format">%1$s постави вашата публикация в любими</string> + <string name="notification_reblog_format">%1$s сподели вашата публикация</string> + <string name="footer_empty">Нищо тук. Дръпнете надолу, за да опресните!</string> + <string name="message_empty">Нищо тук.</string> + <string name="post_content_show_less">Сгъване</string> + <string name="post_content_show_more">Разгъване</string> + <string name="post_content_warning_show_less">Покажи по-малко</string> + <string name="post_content_warning_show_more">Покажи повече</string> + <string name="post_sensitive_media_directions">Щракнете за преглед</string> + <string name="post_media_hidden_title">Мултимедията е скрита</string> + <string name="post_sensitive_media_title">Деликатно съдържание</string> + <string name="post_boosted_format">%1$s сподели</string> + <string name="post_username_format">\@%1$s</string> + <string name="title_licenses">Лицензи</string> + <string name="title_announcements">Оповестявания</string> + <string name="title_scheduled_posts">Планирани публикации</string> + <string name="title_drafts">Чернови</string> + <string name="title_edit_profile">Редакция на профила ви</string> + <string name="title_follow_requests">Заявки за последване</string> + <string name="title_domain_mutes">Скрити домейни</string> + <string name="title_blocks">Блокирани потребители</string> + <string name="title_mutes">Заглушени потребители</string> + <string name="title_bookmarks">Отметки</string> + <string name="title_favourites">Любими</string> + <string name="title_followers">Последователи</string> + <string name="title_follows">Последвани</string> + <string name="title_posts_pinned">Закачени</string> + <string name="title_posts_with_replies">С отговори</string> + <string name="title_posts">Публикации</string> + <string name="title_tab_preferences">Раздели</string> + <string name="title_direct_messages">Директни съобщения</string> + <string name="title_public_local">Местни</string> + <string name="title_notifications">Известия</string> + <string name="title_home">Начало</string> + <string name="error_sender_account_gone">Грешка при изпращане на публикация.</string> + <string name="error_media_upload_sending">Качването бе неуспешно.</string> + <string name="error_media_upload_image_or_video">Изображения и видеоклипове не могат да бъдат прикачени към една и съща публикация.</string> + <string name="error_media_download_permission">Изисква се разрешение за съхранение на мултимедия.</string> + <string name="error_media_upload_permission">Изисква се разрешение за четене на мултимедия.</string> + <string name="error_media_upload_opening">Този файл не можа да бъде отворен.</string> + <string name="error_media_upload_type">Този тип файл не може да бъде качен.</string> + <string name="error_compose_character_limit">Публикацията е твърде дълга!</string> + <string name="error_retrieving_oauth_token">Получаването на токен за вход бе неуспешно. Ако това продължава, опитайте да се впишете в браузъра от менюто.</string> + <string name="error_authorization_denied">Упълномощаването е отказано. Ако сте сигурни, че сте предоставили правилните данни, опитайте да се впишете в браузъра от менюто.</string> + <string name="error_authorization_unknown">Възникна неидентифицирана грешка при упълномощаване. Ако това продължава, опитайте да се впишете в браузъра от менюто.</string> + <string name="error_no_web_browser_found">Неуспешно намиране на уеб браузър, който да се използва.</string> + <string name="error_failed_app_registration">Неуспешно удостоверяване с тази инстанция. Ако това продължава, опитайте да се впишете в браузъра от менюто.</string> + <string name="error_invalid_domain">Въведен е невалиден домейн</string> + <string name="error_empty">Това не може да бъде празно.</string> + <string name="error_network">Възникна грешка в мрежата. Моля, проверете връзката си и опитайте отново.</string> + <string name="error_generic">Възникна грешка.</string> + <string name="draft_deleted">Черновата е изтрита</string> + <string name="drafts_failed_loading_reply">Неуспешно зареждане на информация за отговор</string> + <string name="drafts_post_failed_to_send">Тази публикация не успя да се изпрати!</string> + <string name="dialog_delete_list_warning">Наистина ли искате да изтриете списъка %1$s\?</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Не можете да качите повече от %1$d мултимедиен прикачен файл.</item> + <item quantity="other">Не можете да качите повече от %1$d мултимедийни прикачени файлове.</item> + </plurals> + <string name="wellbeing_hide_stats_profile">Скриване на количествена статистика на профили</string> + <string name="wellbeing_hide_stats_posts">Скриване на количествена статистика на публикации</string> + <string name="limit_notifications">Ограничаване на известия от емисия</string> + <string name="review_notifications">Преглед на известията</string> + <string name="wellbeing_mode_notice">Част от информацията, която може да повлияе на вашето психично състояние, ще бъде скрита. Това включва: +\n +\n - Известия за Любими/Споделяния/Последвани +\n - Брой Любими/Споделяния на публикации +\n - Статистика за Последователи/Публикации на профили +\n +\n Изскачащите известия няма да бъдат засегнати, но можете да прегледате предпочитанията си за известяване ръчно.</string> + <string name="account_note_saved">Запазено!</string> + <string name="account_note_hint">Вашата лична бележка за този акаунт</string> + <string name="pref_title_wellbeing_mode">Благосъстояние</string> + <string name="pref_title_hide_top_toolbar">Скриване на заглавието на горната лента с инструменти</string> + <string name="pref_title_confirm_reblogs">Питане за потвърждение преди споделяне</string> + <string name="pref_title_show_cards_in_timelines">Показване на визуализации на връзки в емисии</string> + <string name="warning_scheduling_interval">Mastodon има минимален интервал за планиране от 5 минути.</string> + <string name="no_announcements">Няма оповестявания.</string> + <string name="no_scheduled_posts">Нямате планирани публикации.</string> + <string name="no_drafts">Нямате чернови.</string> + <string name="post_lookup_error_format">Грешка при търсенето на публикация %1$s</string> + <string name="edit_poll">Редакция</string> + <string name="poll_new_choice_hint">Избор %1$d</string> + <string name="poll_allow_multiple_choices">Множество избора</string> + <string name="add_poll_choice">Добавяне на избор</string> + <string name="duration_7_days">7 дни</string> + <string name="duration_3_days">3 дни</string> + <string name="duration_1_day">1 ден</string> + <string name="duration_6_hours">6 часа</string> + <string name="notification_boost_name">Споделяния</string> + <string name="notification_follow_request_description">Известия за заявки за последване</string> + <string name="notification_follow_request_name">Заявки за последване</string> + <string name="notification_follow_description">Известия за нови последователи</string> + <string name="notification_follow_name">Нови последователи</string> + <string name="notification_mention_descriptions">Известия за нови споменавания</string> + <string name="notification_mention_name">Нови споменавания</string> + <string name="post_text_size_largest">Най-голям</string> + <string name="post_text_size_large">Голям</string> + <string name="post_text_size_medium">Среден</string> + <string name="post_text_size_small">Малък</string> + <string name="visibility_unlisted">Скрито: Не се показва в публични емисии</string> + <string name="pref_post_text_size">Размер на текста на публикация</string> + <string name="post_privacy_followers_only">Само за последователи</string> + <string name="post_privacy_unlisted">Скрито</string> + <string name="post_privacy_public">Публично</string> + <string name="pref_main_nav_position_option_bottom">Отдолу</string> + <string name="pref_main_nav_position_option_top">Отгоре</string> + <string name="pref_main_nav_position">Основна навигационна позиция</string> + <string name="pref_failed_to_sync">Синхронизирането на предпочитанията бе неуспешно</string> + <string name="pref_publishing">Публикуване (синхронизирано със сървър)</string> + <string name="pref_default_media_sensitivity">Винаги маркиране на мултимедия като чувствителна</string> + <string name="pref_default_post_privacy">Поверителност на публикация по подразбиране</string> + <string name="pref_title_http_proxy_port">HTTP прокси порт</string> + <string name="pref_title_http_proxy_server">HTTP прокси сървър</string> + <string name="pref_title_http_proxy_enable">Активиране на HTTP прокси</string> + <string name="pref_title_http_proxy_settings">HTTP прокси</string> + <string name="pref_title_proxy_settings">Прокси</string> + <string name="pref_title_show_media_preview">Изтегляне на визуализации за мултимедии</string> + <string name="pref_title_show_replies">Показване на отговори</string> + <string name="pref_title_show_boosts">Показване на споделяния</string> + <string name="error_multimedia_size_limit">Видео и аудио файловете не може да превишават %1$s МБ в размер.</string> + <string name="error_image_edit_failed">Изображението не може да бъде редактирано.</string> + <string name="a11y_label_loading_thread">Зареждане на нишка</string> + <string name="pref_ui_text_size">Размер на текста на интерфейса</string> + <string name="dialog_follow_hashtag_hint">#хаштаг</string> + <string name="dialog_follow_hashtag_title">Последване на хаштаг</string> + <string name="label_image">Изображение</string> + <string name="hint_media_description_missing">Мултимедията трябва да има описание.</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="title_login">Вписване</string> + <string name="action_unsubscribe_account">Отписване</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="tusky_compose_post_quicksetting_label">Композиране на публикация</string> + <string name="title_edits">Редакции</string> + <string name="notification_sign_up_description">Известия за нови потребители</string> + <string name="notification_sign_up_name">Регистрации</string> + <string name="action_post_failed">Неуспешно качване</string> + <string name="post_edited">Редактирано %1$s</string> + <string name="notification_header_report_format">%1$s докладва %2$s</string> + <string name="notification_report_format">Ново докладване на %1$s</string> + <string name="action_delete_conversation">Изтриване на разговор</string> + <string name="post_media_image">Изображение</string> + <string name="duration_60_days">60 дни</string> + <string name="dialog_delete_conversation_warning">Да се изтрие ли този разговор?</string> + <string name="pref_show_self_username_always">Винаги</string> + <string name="pref_default_post_language">Език на публикуване по подразбиране</string> + <string name="pref_show_self_username_never">Никога</string> + <string name="pref_summary_http_proxy_invalid"><невалидно></string> + <string name="notification_report_name">Докладвания</string> + <string name="about_account_info_title">Вашият акаунт</string> + <string name="about_device_info_title">Вашето устройство</string> + <string name="notification_listenable_worker_description">Известия, когато Tusky работи във фонов режим</string> + <string name="error_following_hashtags_unsupported">Тази инстанция не поддържа следване на хаштагове.</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="action_subscribe_account">Абониране</string> + <string name="pref_summary_http_proxy_missing"><не е зададено></string> + <string name="instance_rule_info">С вписването вие се съгласявате с правилата на %1$s.</string> + <string name="error_muting_hashtag_format">Грешка при заглушаване на #%1$s</string> + <string name="error_unmuting_hashtag_format">Грешка при раззаглушаване на #%1$s</string> + <string name="action_unbookmark">Премахване на отметка</string> + <string name="action_dismiss">Отхвърляне</string> + <string name="action_continue_edit">Продължаване на редактирането</string> + <string name="action_discard">Отхвърляне на промените</string> + <string name="action_details">Подробности</string> + <string name="duration_no_change">(Няма промяна)</string> + <string name="post_media_alt">Алт текст</string> + <string name="description_post_language">Езика на публикация</string> + <string name="action_browser_login">Вписване с Браузър</string> + <string name="pref_title_notification_filter_sign_ups">някой се регистрира</string> + <string name="status_created_at_now">сега</string> + <string name="description_post_edited">Редактирано</string> + <string name="action_refresh">Опресняване</string> + <string name="action_add_reaction">добавяне на реакция</string> + <string name="status_count_one_plus">1+</string> + <string name="account_username_copied">Потребителското име бе копирано</string> + <string name="failed_to_pin">Неуспешно закачане</string> + <string name="failed_to_unpin">Неуспешно разкачане</string> + <string name="notification_update_name">Редакции на публикации</string> + <string name="pref_reading_order_newest_first">Първо най-новите</string> + <string name="pref_reading_order_oldest_first">Първо най-старите</string> + <string name="pref_title_confirm_favourites">Питане за потвърждение преди слагане в любими</string> + <string name="dialog_delete_filter_positive_action">Изтриване</string> + <string name="dialog_delete_filter_text">Да се изтрие ли филтър \'%1$s\'?</string> + <string name="no_lists">Нямате списъци.</string> + <string name="pref_title_notification_filter_reports">има ново докладване</string> + <string name="error_following_hashtag_format">Грешка при последване на #%1$s</string> + <string name="error_unfollowing_hashtag_format">Грешка при отследване на #%1$s</string> + <string name="title_public_federated">Федерирани</string> + <string name="select_list_manage">Управление на списъци</string> + <string name="notification_update_format">%1$s редактира публикацията си</string> + <string name="pref_title_account_filter_keywords">Профили</string> + <string name="filter_description_hide">Пълно скриване</string> + <string name="duration_30_days">30 дни</string> + <string name="title_public_trending_statuses">Налагащи се публикации</string> + <string name="filter_action_warn">Предупреди</string> + <string name="status_edit_info">Редактирано: %1$s</string> + <string name="action_add">Добавяне</string> + <string name="filter_keyword_addition_title">Добавяне на ключова дума</string> + <string name="filter_edit_keyword_title">Редакция на ключова дума</string> + <string name="filter_keyword_display_format">%1$s (цяла дума)</string> + <string name="filter_action_hide">Скриване</string> + <string name="filter_description_warn">Скриване с предупреждение</string> + <string name="duration_90_days">90 дни</string> + <string name="duration_180_days">180 дни</string> + <string name="duration_365_days">365 дни</string> + <string name="instance_rule_title">%1$s правила</string> + <string name="label_filter_title">Заглавие</string> + <string name="error_blocking_domain">Неуспешно заглушаване на %1$s: %2$s</string> + <string name="error_unblocking_domain">Неуспешно раззаглушаване на %1$s: %2$s</string> + <string name="confirmation_hashtag_unfollowed">#%1$s отследвано</string> + <string name="list_exclusive_label">Скриване от началната емисия</string> + <string name="title_public_trending_hashtags">Налагащи се хаштагове</string> + <string name="action_post_failed_do_nothing">Отхвърляне</string> + <string name="error_media_upload_sending_fmt">Качването бе неуспешно: %1$s</string> + <string name="title_followed_hashtags">Последвани хаштагове</string> + <string name="notification_sign_up_format">%1$s се регистрира</string> + <string name="action_post_failed_show_drafts">Покажи чернови</string> + <string name="duration_14_days">14 дни</string> + <string name="report_category_violation">Нарушение на правилата</string> + <string name="action_edit_image">Редакция на изображение</string> + <string name="notification_update_description">Известия, когато публикации с които сте взаимодействали, са редактирани</string> + <string name="load_newest_notifications">Зареждане на най-новите известия</string> + <string name="compose_delete_draft">Изтриване на чернова\?</string> + <string name="action_unfollow_hashtag_format">Отследване на #%1$s\?</string> + <string name="mute_notifications_switch">Заглушаване на известията</string> + <string name="action_translate">Превеждане</string> + <string name="label_translated">Преведено от %1$s чрез %2$s</string> + <string name="report_category_spam">Спам</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-bn-rBD/strings.xml b/app/src/main/res/values-bn-rBD/strings.xml new file mode 100644 index 0000000..c83cb7a --- /dev/null +++ b/app/src/main/res/values-bn-rBD/strings.xml @@ -0,0 +1,503 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="conversation_1_recipients">%1$s</string> + <string name="abbreviated_seconds_ago">%1$dসে</string> + <string name="description_poll">পছন্দগুলি সহ নর্বাচন: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="description_visibility_direct">সরাসরি</string> + <string name="description_visibility_private">অনুগামিবৃন্দ</string> + <string name="description_visibility_unlisted">অতালিকাভুক্ত</string> + <string name="description_visibility_public">সর্বজনীন</string> + <string name="description_post_favourited">পছন্দ</string> + <string name="description_post_reblogged">আবার ব্লগ</string> + <string name="description_post_media_no_description_placeholder">বর্ণনা নাই</string> + <string name="description_post_cw">সতর্কবার্তা: %1$s</string> + <string name="description_post_media">মিডিয়া: %1$s</string> + <string name="title_favourited_by">দ্বারা পছন্দ</string> + <string name="title_reblogged_by">দ্বারা সর্মথন</string> + <string name="pin_action">পিন</string> + <string name="unpin_action">আনপিন</string> + <string name="label_remote_account">নীচের তথ্য অসম্পূর্ণভাবে ব্যবহারকারীর প্রোফাইল প্রতিফলিত হতে পারে। ব্রাউজারে সম্পূর্ণ প্রোফাইল খুলতে টিপুন।</string> + <string name="pref_title_absolute_time">পরম সময় ব্যবহার করুন</string> + <string name="profile_metadata_content_label">উপাদান</string> + <string name="profile_metadata_label_label">লেবেল</string> + <string name="profile_metadata_add">তথ্য যোগ করুন</string> + <string name="profile_metadata_label">প্রোফাইল মেটাডেটা</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_apache_2">এপাচে লাইসেন্সের অধীনে লাইসেন্স (নীচের কপি)</string> + <string name="license_description">টাস্কি নিম্নলিখিত ওপেন সোর্স প্রকল্প থেকে কোড এবং সম্পদ রয়েছে:</string> + <string name="unreblog_private">সমর্থন ফেরত</string> + <string name="reblog_private">মূল শ্রোতাদেড় সমথন পাঠাও</string> + <string name="profile_badge_bot_text">বট</string> + <string name="download_failed">ডাউনলোড ব্যর্থ হয়েছে</string> + <string name="caption_notoemoji">গুগল এর বর্তমান ইমোজি সেট</string> + <string name="caption_twemoji">ম্যাস্টোডোন এর মান ইমোজি সেট</string> + <string name="caption_blobmoji">অ্যান্ড্রয়েড ৪.৪-৭.১ থেকে পরিচিত ব্লোব ইমোজিস</string> + <string name="caption_systememoji">আপনার ডিভাইসের ডিফল্ট ইমোজি সেট</string> + <string name="restart">পুনরারম্ভ</string> + <string name="later">পরবর্তীতে</string> + <string name="restart_emoji">এই পরিবর্তনগুলি প্রয়োগ করার জন্য আপনাকে টাস্কি পুনরায় চালু করতে হবে</string> + <string name="restart_required">অ্যাপ্লিকেশন পুনরায় আরম্ভ করা প্রয়োজন</string> + <string name="action_open_post">টুট খুলুন</string> + <string name="expand_collapse_all_posts">সমস্ত স্টেটাস প্রসারিত/সংকুচিত করুন</string> + <string name="performing_lookup_title">অনুসন্ধান করা হচ্ছে …</string> + <string name="download_fonts">আপনাকে প্রথমে এই ইমোজি সেটগুলি ডাউনলোড করতে হবে</string> + <string name="system_default">সিস্টেমের ডিফল্ট</string> + <string name="emoji_style">ইমোজি স্টাইল</string> + <string name="error_no_custom_emojis">আপনার ইনস্ট্যান্স %1$s এর কোনো কাস্টম ইমোজিস নেই</string> + <string name="action_compose_shortcut">রচনা</string> + <string name="send_post_notification_saved_content">টুট এর একটি কপি আপনার ড্রাফটে সংরক্ষণ করা হয়েছে</string> + <string name="send_post_notification_cancel_title">পাঠানো বাতিল</string> + <string name="send_post_notification_channel_name">টুট পাঠানো হচ্ছে</string> + <string name="send_post_notification_error_title">টুট পাঠাতে গিয়ে একটি ত্রুটি ঘটেছে</string> + <string name="send_post_notification_title">টুট পাঠানো হচ্ছে …</string> + <string name="compose_save_draft">ড্রাফট সংরক্ষণ\?</string> + <string name="lock_account_label_description">অনুসারী অনুমোদন করার জন্য আপনাকে প্রয়োজন</string> + <string name="lock_account_label">অ্যাকাউন্ট লক করুন</string> + <string name="action_set_caption">ক্যাপশন সেট করুন</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">দৃষ্টি প্রতিবন্ধী জন্য বর্ণনা করুন +\n(%1$d অক্ষর সীমা)</item> + </plurals> + <string name="compose_active_account_description">অ্যাকাউন্ট %1$s থেকে পোস্ট করা হচ্ছে</string> + <string name="action_remove_from_list">তালিকা থেকে অ্যাকাউন্ট সরান</string> + <string name="action_add_to_list">তালিকায় অ্যাকাউন্ট যোগ করুন</string> + <string name="hint_search_people_list">আপনি অনুসরণ মানুষের জন্য অনুসন্ধান করুন</string> + <string name="action_delete_list">তালিকা মুছে দিন</string> + <string name="action_rename_list">তালিকা পুনঃ নামকরণ কর</string> + <string name="action_create_list">একটি তালিকা তৈরি করুন</string> + <string name="error_delete_list">তালিকা মুছে ফেলা যায়নি</string> + <string name="error_rename_list">তালিকা নামকরণ করা যায়নি</string> + <string name="error_create_list">তালিকা তৈরি করা যায়নি</string> + <string name="title_lists">তালিকাসমূহ</string> + <string name="action_lists">তালিকাসমূহ</string> + <string name="add_account_description">নতুন ম্যাস্টোডোন অ্যাকাউন্ট যোগ করুন</string> + <string name="add_account_name">অ্যাকাউন্ট যোগ করুন</string> + <string name="filter_add_description">বাক্য ফিল্টার কর</string> + <string name="filter_dialog_update_button">আপডেট</string> + <string name="filter_dialog_remove_button">সরাও</string> + <string name="filter_edit_title">ফিল্টার সম্পাদনা করুন</string> + <string name="filter_addition_title">ফিল্টার যোগ করুন</string> + <string name="pref_title_thread_filter_keywords">কথাবার্তা</string> + <string name="pref_title_public_filter_keywords">পাবলিক টাইমলাইন</string> + <string name="load_more_placeholder_text">আরো লোড কর</string> + <string name="replying_to">\'@%1$s কে উত্তর দিচ্ছে\'</string> + <string name="title_media">মিডিয়া</string> + <string name="pref_title_alway_show_sensitive_media">সর্বদা সংবেদনশীল কন্টেন্ট প্রদর্শন করুন</string> + <string name="follows_you">আপনাকে অনুসরন করে</string> + <string name="abbreviated_in_seconds">\'%1$ds এ\'</string> + <string name="abbreviated_in_minutes">\'%1$dm এ\'</string> + <string name="abbreviated_in_hours">\'%1$dh এ\'</string> + <string name="abbreviated_in_days">\'%1$dd এ\'</string> + <string name="abbreviated_in_years">\'%1$dy এ\'</string> + <string name="state_follow_requested">অনুরোধ অনুসরণ করুন</string> + <string name="post_media_video">ভিডিও</string> + <string name="post_media_images">চিত্রগুলি</string> + <string name="post_share_link">টুট এর সাথে লিংক ভাগ করুন</string> + <string name="post_share_content">টুট এর কন্টেন্ট ভাগ করুন</string> + <string name="about_tusky_account">টাস্কির প্রোফাইল</string> + <string name="about_bug_feature_request_site">বাগ রিপোর্ট এবং বৈশিষ্ট্য অনুরোধ: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_project_site">প্রকল্প ওয়েবসাইট: +\nhttps://tusky.app</string> + <string name="about_tusky_license">টাস্কি মুক্ত এবং ওপেন সোর্স সফ্টওয়্যার। এটি GNU জেনারেল পাবলিক লাইসেন্স সংস্করণ 3 এর অধীনে লাইসেন্সযুক্ত। আপনি এখানে লাইসেন্স দেখতে পারেন: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="about_tusky_version">টাস্কি %1$s</string> + <string name="about_title_activity">সম্পর্কিত</string> + <string name="description_account_locked">লক অ্যাকাউন্ট</string> + <string name="notification_poll_description">শেষ হয়েছে যে নির্বাচনগুলির সম্পর্কে বিজ্ঞপ্তি</string> + <string name="notification_poll_name">নির্বাচনগুলি</string> + <string name="notification_favourite_description">আপনার টুটগুলি প্রিয় হিসাবে চিহ্নিত পেতে বিজ্ঞপ্তি</string> + <string name="notification_favourite_name">প্রিয়গুলো</string> + <string name="notification_boost_description">আপনার টুটগুলি সমর্থন হলে বিজ্ঞপ্তিগুলি</string> + <string name="notification_boost_name">সমর্থনগুলি</string> + <string name="notification_follow_description">নতুন অনুসরণকারী সম্পর্কে বিজ্ঞপ্তি</string> + <string name="notification_follow_name">নতুন অনুসরণকারী</string> + <string name="notification_mention_descriptions">নতুন উল্লেখ সম্পর্কে বিজ্ঞপ্তি</string> + <string name="notification_mention_name">নতুন উল্লেখসমূহ</string> + <string name="post_text_size_largest">বৃহত্তম</string> + <string name="post_text_size_large">বড়</string> + <string name="post_text_size_medium">মাঝারি</string> + <string name="post_text_size_small">ছোট</string> + <string name="post_text_size_smallest">কনিষ্ঠ</string> + <string name="pref_post_text_size">স্থিতি টেক্সট আকার</string> + <string name="post_privacy_followers_only">শুধুমাত্র অনুগামিবৃন্দ</string> + <string name="post_privacy_unlisted">অতালিকাভুক্ত</string> + <string name="post_privacy_public">সর্বজনীন</string> + <string name="pref_failed_to_sync">সেটিংস সিঙ্ক করতে ব্যর্থ</string> + <string name="pref_publishing">প্রকাশনা (সার্ভারের সাথে সিঙ্ক করা)</string> + <string name="pref_default_media_sensitivity">সর্বদা সংবেদনশীল হিসাবে মিডিয়া চিহ্নিত করুন</string> + <string name="pref_default_post_privacy">ডিফল্ট পোস্ট গোপনীয়তা</string> + <string name="pref_title_http_proxy_port">HTTP প্রক্সি পোর্ট</string> + <string name="pref_title_http_proxy_server">HTTP প্রক্সি সার্ভার</string> + <string name="pref_title_http_proxy_enable">HTTP প্রক্সি সক্ষম কর</string> + <string name="pref_title_http_proxy_settings">HTTP প্রক্সি</string> + <string name="pref_title_proxy_settings">প্রক্সি</string> + <string name="pref_title_show_media_preview">মিডিয়া পূর্বরূপ ডাউনলোড করুন</string> + <string name="pref_title_show_replies">উত্তর প্রদর্শন করুন</string> + <string name="pref_title_show_boosts">সমর্থন দেখান</string> + <string name="pref_title_post_tabs">ট্যাবগুলি</string> + <string name="pref_title_post_filter">টাইমলাইন ফিল্টারিং</string> + <string name="pref_title_animate_gif_avatars">GIF অবতার অ্যানিমেশন করুন</string> + <string name="pref_title_bot_overlay">বট জন্য সূচক প্রদর্শন করুন</string> + <string name="pref_title_language">ভাষা</string> + <string name="pref_title_custom_tabs">ক্রোম কাস্টম ট্যাব ব্যবহার করুন</string> + <string name="pref_title_browser_settings">ব্রাউজার</string> + <string name="app_theme_system">সিস্টেম ডিজাইন ব্যবহার করুন</string> + <string name="app_theme_auto">সূর্যাস্ত স্বয়ংক্রিয়</string> + <string name="app_theme_black">কালো</string> + <string name="app_theme_light">আলো</string> + <string name="app_them_dark">অন্ধকার</string> + <string name="pref_title_timeline_filters">ফিল্টার</string> + <string name="pref_title_timelines">টাইমলাইন</string> + <string name="pref_title_app_theme">এপ্লিকেশন এর থিম</string> + <string name="pref_title_appearance_settings">উপস্থিতি</string> + <string name="pref_title_notification_filter_poll">নির্বাচন শেষ হয়েছে</string> + <string name="pref_title_notification_filter_favourites">আমার পোস্টগুলির পছন্দ হচ্ছে</string> + <string name="pref_title_notification_filter_reblogs">আমার পোস্টগুলির সমর্থন হচ্ছে</string> + <string name="pref_title_notification_filter_follows">অনুসরণ</string> + <string name="pref_title_notification_filter_mentions">উল্লিখিত</string> + <string name="pref_title_notification_filters">যখন আমাকে অবহিত</string> + <string name="pref_title_notification_alert_light">আলো সঙ্গে অবহিত</string> + <string name="pref_title_notification_alert_vibrate">কম্পন সঙ্গে অবহিত</string> + <string name="pref_title_notification_alert_sound">একটি শব্দ সঙ্গে অবহিত</string> + <string name="pref_title_notification_alerts">সতর্কতা</string> + <string name="pref_title_notifications_enabled">বিজ্ঞপ্তিগুলি</string> + <string name="pref_title_edit_notification_settings">বিজ্ঞপ্তিগুলি</string> + <string name="visibility_direct">সরাসরি: শুধুমাত্র উল্লেখ ব্যবহারকারীদের পোস্ট করুন</string> + <string name="visibility_private">শুধুমাত্র অনুসরণকারীদের: শুধুমাত্র অনুসরণকারীদের পোস্ট করুন</string> + <string name="visibility_unlisted">তালিকাভুক্ত নয়: সর্বজনীন সময়সূচীগুলিতে দেখাবেন না</string> + <string name="visibility_public">সর্বজনীন: পাবলিক টাইমলাইনে পোস্ট কর</string> + <string name="dialog_redraft_post_warning">এই টুট টি মুছে ফেলবেন এবং পুনরায় ড্রাফট করবেন\?</string> + <string name="dialog_delete_post_warning">এই টুট টি মুছে ফেলবেন\?</string> + <string name="dialog_unfollow_warning">এই অ্যাকাউন্টটি অনুসরণ করবেন না\?</string> + <string name="dialog_message_cancel_follow_request">অনুসরণ অনুরোধ প্রত্যাহার\?</string> + <string name="dialog_download_image">ডাউনলোড</string> + <string name="dialog_message_uploading_media">আপলোড হচ্ছে …</string> + <string name="dialog_title_finishing_media_upload">মিডিয়া আপলোড সমাপ্ত করা হচ্ছে</string> + <string name="dialog_whats_an_instance">কোনও উদাহরণের ঠিকানা বা ডোমেন এখানে প্রবেশ করা যেতে পারে যেমন mastodon.social, icosahedron.website, social.tchncs.de, এবং <a href=\"https://instances.social\"> আরও! </a> +\n +\nআপনার যদি এখনো অ্যাকাউন্ট না থাকে তবে আপনি যে ইনস্ট্যান্সটিতে যোগ দিতে চান সেটির নামটি প্রবেশ করতে এবং সেখানে একটি অ্যাকাউন্ট তৈরি করতে পারেন। +\n +\n +\n +\nএকটি ইনস্ট্যান্স একটি একক স্থান যেখানে আপনার অ্যাকাউন্ট হোস্ট করা হয়, তবে আপনি সহজেই যোগাযোগ করতে পারেন এবং অন্যান্য ক্ষেত্রে যেমন আপনি একই সাইটে ছিলেন তা অনুসরণ করতে পারেন। +\n +\n +\n +\nআরো তথ্য <a href=\"https://joinmastodon.org\"> joinmastodon.org </a> এ পাওয়া যেতে পারে। <a href="https://instances.social">more!</a> +\n +\nIf you don\'t yet have an account, you can enter the name of the instance you\'d like to join and create an account there. +\n +\nAn instance is a single place where your account is hosted, but you can easily communicate with and follow folks on other instances as though you were on the same site. +\n +\nMore info can be found at <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="login_connection">সংযুক্ত হচ্ছে …</string> + <string name="link_whats_an_instance">ইনস্ট্যান্স কি\?</string> + <string name="label_header">হেডার</string> + <string name="label_avatar">অবতার</string> + <string name="label_quick_reply">উত্তর…</string> + <string name="search_no_results">কোন ফলাফল নেই</string> + <string name="hint_search">অনুসন্ধান…</string> + <string name="hint_note">জীবনী</string> + <string name="hint_display_name">প্রদর্শন নাম</string> + <string name="hint_content_warning">সতর্কবার্তা</string> + <string name="hint_compose">কি হচ্ছে\?</string> + <string name="hint_domain">কোন ইনস্ট্যান্স\?</string> + <string name="confirmation_unmuted">ব্যবহারকারী সশব্দ</string> + <string name="confirmation_unblocked">ব্যবহারকারী অবরোধ মুক্ত</string> + <string name="confirmation_reported">পাঠানো হয়েছে!</string> + <string name="send_media_to">মিডিয়া শেয়ার করুন …</string> + <string name="send_post_content_to">টুট ভাগ করুন …</string> + <string name="send_post_link_to">টুট URL ভাগ করুন …</string> + <string name="downloading_media">মিডিয়া ডাউনলোড করা হচ্ছে</string> + <string name="download_media">মিডিয়া ডাউনলোড করুন</string> + <string name="action_share_as">হিসাবে ভাগ করুন …</string> + <string name="action_open_as">\'%1$s হিসাবে খুলুন\'</string> + <string name="action_copy_link">লিঙ্ক অনুলিপি করুন</string> + <string name="download_image">\'%1$s ডাউনলোড হচ্ছে\'</string> + <string name="action_open_media_n">মিডিয়া খুলুন #%1$d</string> + <string name="title_links_dialog">লিংকসমূহ</string> + <string name="title_mentions_dialog">উল্লেখসমূহ</string> + <string name="title_hashtags_dialog">হ্যাশট্যাগ</string> + <string name="action_open_faved_by">প্রিয়গুলি দেখান</string> + <string name="action_open_reblogged_by">সমর্থন দেখান</string> + <string name="action_open_reblogger">সমর্থক লেখক খুলুন</string> + <string name="action_hashtags">হ্যাশট্যাগ</string> + <string name="action_mentions">উল্লেখসমূহ</string> + <string name="action_links">লিংকসমূহ</string> + <string name="action_add_tab">ট্যাব যুক্ত করুন</string> + <string name="action_emoji_keyboard">ইমোজি কীবোর্ড</string> + <string name="action_content_warning">সতর্কবার্তা</string> + <string name="action_toggle_visibility">টুট দৃশ্যমানতা</string> + <string name="action_access_drafts">ড্রাফটগুলি</string> + <string name="action_search">অনুসন্ধান</string> + <string name="action_reject">প্রত্যাখ্যান</string> + <string name="action_accept">গ্রহণ</string> + <string name="action_undo">বাতিল</string> + <string name="action_edit_own_profile">সম্পাদন</string> + <string name="action_edit_profile">প্রোফাইল সম্পাদনা করো</string> + <string name="action_save">সংরক্ষণ</string> + <string name="action_open_drawer">ড্রয়ার খুলুন</string> + <string name="action_hide_media">মিডিয়া লুকান</string> + <string name="action_mention">উল্লেখ</string> + <string name="action_unmute">সশব্দ</string> + <string name="action_mute">শব্দ বন্ধ</string> + <string name="action_share">ভাগ</string> + <string name="action_photo_take">ছবি তোল</string> + <string name="action_add_media">মিডিয়া যোগ করুন</string> + <string name="action_open_in_web">ব্রাউজারে খোলা</string> + <string name="action_view_media">মিডিয়া</string> + <string name="action_view_follow_requests">অনুরোধ অনুসরণ করুন</string> + <string name="action_view_blocks">অবরুদ্ধ ব্যবহারকারী</string> + <string name="action_view_mutes">শব্দ বন্ধ ব্যবহারকারী</string> + <string name="action_view_favourites">প্রিয়গুলি</string> + <string name="action_view_account_preferences">অ্যাকাউন্টের পছন্দসমূহ</string> + <string name="action_view_preferences">পছন্দসমূহ</string> + <string name="action_view_profile">প্রোফাইল</string> + <string name="action_close">বদ্ধ</string> + <string name="action_retry">পুনরায় চেষ্টা কর</string> + <string name="action_send_public">টুট!</string> + <string name="action_send">টুট</string> + <string name="action_delete_and_redraft">মুছুন এবং পুনরায় ড্রাফট কর</string> + <string name="action_delete">মুছে ফেল</string> + <string name="action_report">রিপোর্ট</string> + <string name="action_show_reblogs">সমর্থন দেখান</string> + <string name="action_hide_reblogs">সমর্থন লুকান</string> + <string name="action_unblock">অবরোধ মুক্ত</string> + <string name="action_block">অবরুদ্ধ</string> + <string name="action_unfollow">অনুসরণ করা বন্ধ করুন</string> + <string name="action_follow">অনুসরণ কর</string> + <string name="action_logout_confirm">আপনি কি অ্যাকাউন্ট %1$s থেকে লগ আউট করতে চান\?</string> + <string name="action_logout">লগ আউট</string> + <string name="action_login">মাস্টোডনের সঙ্গে লগইন করো</string> + <string name="action_compose">রচনা</string> + <string name="action_more">অধিক</string> + <string name="action_unfavourite">পছন্দ সরান</string> + <string name="action_favourite">পছন্দ</string> + <string name="action_unreblog">সমর্থন সরান</string> + <string name="action_reblog">সমর্থন</string> + <string name="action_reply">উত্তর</string> + <string name="action_quick_reply">দ্রুত উত্তর</string> + <string name="report_comment_hint">অতিরিক্ত মন্তব্যগুলি\?</string> + <string name="report_username_format">রিপোর্ট @%1$s</string> + <string name="footer_empty">এখানে কিছু নেই. রিফ্রেশ করতে নিচে টানুন!</string> + <string name="message_empty">এখানে কিছুই নেই।</string> + <string name="post_content_show_less">বন্ধ</string> + <string name="post_content_show_more">বিস্তৃত</string> + <string name="post_content_warning_show_less">প্রদর্শন কম</string> + <string name="post_content_warning_show_more">আরও দেখাও</string> + <string name="post_sensitive_media_directions">দেখার জন্য ক্লিক করুন</string> + <string name="post_media_hidden_title">মিডিয়া লুকানো</string> + <string name="post_sensitive_media_title">সংবেদনশীল কন্টেন্ট</string> + <string name="title_licenses">লাইসেন্সগুলি</string> + <string name="title_drafts">খসড়া</string> + <string name="title_edit_profile">আপনার প্রোফাইল সম্পাদনা করুন</string> + <string name="title_follow_requests">অনুরোধ অনুসরণ করুন</string> + <string name="title_blocks">অবরুদ্ধ ব্যবহারকারী</string> + <string name="title_mutes">শব্দ বন্ধ ব্যবহারকারী</string> + <string name="abbreviated_days_ago">%1$dদি</string> + <string name="abbreviated_minutes_ago">%1$dমা</string> + <string name="abbreviated_hours_ago">%1$dঘ</string> + <string name="abbreviated_years_ago">%1$dব</string> + <string name="post_boosted_format">%1$s বুস্টকৃত</string> + <string name="post_username_format">\@%1$s</string> + <string name="dialog_mute_hide_notifications">বিজ্ঞপ্তি লুকাও</string> + <string name="action_unmute_desc">আনমিউট %1$s</string> + <string name="action_unmute_domain">আনমিউট %1$s</string> + <string name="pref_main_nav_position_option_bottom">সবচেয়ে শেষ</string> + <string name="pref_main_nav_position_option_top">সর্বপ্রথম</string> + <string name="pref_main_nav_position">মূল ন্যাভিগেশন জায়গা</string> + <string name="pref_title_gradient_for_media">লুকানো মিডিয়ার জন্য রঙিন গ্রেডিয়েন্ট ব্যবহার করি</string> + <string name="hashtags">হ্যাশট্যাগ</string> + <string name="add_hashtag_title">হ্যাশট্যাগ যোগ করো</string> + <string name="pref_title_confirm_reblogs">বুস্ট করার আগে নিশ্চিত করো</string> + <string name="pref_title_show_cards_in_timelines">টাইমলাইনে লিঙ্ক প্রিভিউ দেখাও</string> + <string name="pref_title_enable_swipe_for_tabs">ট্যাবের মাঝে সোয়াইপ সংকেত চালু করো</string> + <string name="notification_follow_request_description">অনুসরণ রিকোয়েস্টের বিজ্ঞপ্তি</string> + <string name="notification_follow_request_name">অনুরোধ অনুসরণ করুন</string> + <string name="pref_title_notification_filter_follow_requests">অনুরোধ অনুসরণ করো</string> + <string name="dialog_mute_warning">নিঃশব্দ @%1$s\?</string> + <string name="dialog_block_warning">অবরুদ্ধ @%1$s\?</string> + <string name="action_unmute_conversation">আলাপ বন্ধ করো</string> + <string name="action_mute_conversation">আলাপ বন্ধ করো</string> + <string name="warning_scheduling_interval">মাস্টোডনের সর্বনিম্ন ৫ মিনিটের সময়সূচীর বিরতি আছে।</string> + <string name="no_drafts">তোমার কোনো খসড়া নেই।</string> + <string name="no_scheduled_posts">তোমার কোনো সময়সূচীত স্ট্যাটাস নেই।</string> + <string name="list">তালিকা</string> + <string name="select_list_title">তালিকা নির্বাচন করো</string> + <string name="description_post_bookmarked">বুকমার্ককৃত</string> + <string name="action_view_bookmarks">বুকমার্কগুলি</string> + <string name="action_bookmark">বুকমার্ক</string> + <string name="title_bookmarks">বুকমার্কগুলি</string> + <string name="about_powered_by_tusky">টাস্কি দ্বারা চালিত</string> + <string name="post_lookup_error_format">\'%1$s পোস্ট অনুসন্ধানে ত্রুটি\'</string> + <string name="action_reset_schedule">রিসেট</string> + <string name="action_schedule_post">নির্ধারিত টুট</string> + <string name="action_access_scheduled_posts">নির্ধারিত টুটগুলি</string> + <string name="action_edit">সম্পাদন</string> + <string name="title_scheduled_posts">নির্ধারিত টুটগুলি</string> + <string name="edit_poll">সম্পাদন</string> + <string name="poll_new_choice_hint">পছন্দ %1$d</string> + <string name="poll_allow_multiple_choices">একাধিক পছন্দ</string> + <string name="add_poll_choice">পছন্দ যুক্ত করুন</string> + <string name="duration_7_days">৭ দিন</string> + <string name="duration_3_days">৩ দিন</string> + <string name="duration_1_day">১ দিন</string> + <string name="duration_6_hours">৬ ঘন্টা</string> + <string name="duration_1_hour">১ ঘন্টা</string> + <string name="duration_30_min">৩০ মিনিট</string> + <string name="duration_5_min">৫ মিনিট</string> + <string name="create_poll_title">ভোটগ্রহণ</string> + <string name="action_remove">সরান</string> + <string name="action_add_poll">পোল যুক্ত করুন</string> + <string name="pref_title_alway_open_spoiler">সর্বদা সামগ্রী সতর্কতা সহ চিহ্নিত টুটগুলি প্রসারিত করুন</string> + <string name="failed_search">অনুসন্ধান করতে ব্যর্থ</string> + <string name="title_accounts">অ্যাকাউন্টগুলো</string> + <string name="filter_dialog_whole_word_description">যখন শব্দ বা বাক্যাংশটি শুধুমাত্র আলফানিউমেরিক হয় তখন এটি শুধুমাত্র তখনই প্রয়োগ করা হবে যদি এটি সম্পূর্ণ শব্দটির সাথে মেলে</string> + <string name="filter_dialog_whole_word">সম্পূর্ণ শব্দ</string> + <string name="mute_domain_warning_dialog_ok">পুরো ডোমেইন লুকান</string> + <string name="mute_domain_warning">আপনি কি সব %1$s ব্লক করতে চান\? আপনি যে ডোমেন থেকে কোনও পাবলিক টাইমলাইনে বা আপনার বিজ্ঞপ্তিগুলিতে সামগ্রী দেখতে পাবেন না। আপনার অনুসরণকারীদের সরানো হবে।</string> + <string name="action_mute_domain">নিঃশব্দ %1$s</string> + <string name="action_view_domain_mutes">গোপন ডোমেইনগুলি</string> + <string name="title_domain_mutes">গোপন ডোমেইনগুলি</string> + <string name="report_description_remote_instance">এই একাউন্ট তা একটি অন্য সার্ভারের। রিপোর্ট এর সঙ্গে একটি বেনামি কপি ওখানে পাঠাতে চান\?</string> + <string name="report_description_1">রিপোর্ট আপনার সার্ভার মডারেটরে পাঠানো হবে। আপনি নীচের এই অ্যাকাউন্টটি কেন প্রতিবেদন করছেন তা ব্যাখ্যা করতে পারেন:</string> + <string name="failed_fetch_posts">স্টেটাসগুলি আনতে ব্যর্থ</string> + <string name="failed_report">রিপোর্ট করতে ব্যর্থ হয়েছে</string> + <string name="report_remote_instance">\'%1$s এ ফরওয়ার্ড করুন\'</string> + <string name="hint_additional_info">অতিরিক্ত মন্তব্যগুলি</string> + <string name="report_sent_success">\'%1$s এ সফলভাবে রিপোর্ট করা হয়েছে\'</string> + <string name="button_done">সম্পন্ন</string> + <string name="button_back">পিছাও</string> + <string name="button_continue">চালিয়ে যাও</string> + <string name="poll_ended_created">আপনি তৈরি একটি নির্বাচন শেষ হয়েছে</string> + <string name="poll_ended_voted">আপনি ভোট দিয়েছেন যে নির্বাচন এ সেটি শেষ হয়েছে</string> + <string name="poll_vote">ভোট</string> + <string name="poll_info_closed">বন্ধ</string> + <string name="poll_info_time_absolute">\'%1$s এ শেষ হবে\'</string> + <string name="poll_info_format"> <!-- 15 votes • 1 hour left --> %1$s • %2$s</string> + <string name="compose_preview_image_description">ছবি %1$s এর জন্য ক্রিয়া</string> + <string name="notification_clear_text">আপনি কি আপনার সমস্ত বিজ্ঞপ্তি স্থায়ীভাবে মুছে ফেলতে চান\?</string> + <string name="compose_shortcut_short_label">রচনা</string> + <string name="compose_shortcut_long_label">টুট রচনা করুন</string> + <string name="filter_apply">প্রয়োগ</string> + <string name="notifications_apply_filter">ফিল্টার</string> + <string name="notifications_clear">পরিষ্কার</string> + <string name="edit_hashtag_hint"># ছাড়া হ্যাশট্যাগ</string> + <string name="hint_list_name">নামের তালিকা</string> + <string name="title_favourites">প্রিয়গুলো</string> + <string name="title_followers">অনুগামিবৃন্দ</string> + <string name="title_follows">অনুসরণ</string> + <string name="title_posts_pinned">পিন করা</string> + <string name="title_posts_with_replies">উত্তরের সাথে</string> + <string name="title_posts">পোস্টগুলি</string> + <string name="title_view_thread">টুট</string> + <string name="title_tab_preferences">ট্যাবগুলি</string> + <string name="title_direct_messages">সরাসরি বার্তা</string> + <string name="title_public_federated">ফেডারেটেড</string> + <string name="title_public_local">স্থানীয়</string> + <string name="title_notifications">বিজ্ঞপ্তিগুলি</string> + <string name="title_home">ঘর</string> + <string name="error_sender_account_gone">টুট পাঠাতে গিয়ে একটি ত্রুটি ঘটেছে।</string> + <string name="error_media_upload_sending">আপলোড করতে ব্যর্থ হয়েছে।</string> + <string name="error_media_upload_image_or_video">ছবি এবং ভিডিও উভয় একই স্টেটাস থেকে সংযুক্ত করা যাবে না।</string> + <string name="error_media_download_permission">মিডিয়া সংরক্ষণ করার অনুমতি প্রয়োজন।</string> + <string name="error_media_upload_permission">মিডিয়া পড়তে অনুমতি প্রয়োজন।</string> + <string name="error_media_upload_opening">ওই ফাইল খোলা যাবে না।</string> + <string name="error_media_upload_type">যে ধরনের ফাইল আপলোড করা যাবে না।</string> + <string name="error_compose_character_limit">এই স্টেটাস টি খুব দীর্ঘ!</string> + <string name="error_retrieving_oauth_token">একটি লগইন টোকেন পেতে ব্যর্থ।</string> + <string name="error_authorization_denied">অনুমোদন অস্বীকার করা হয়েছে।</string> + <string name="error_authorization_unknown">একটি অজ্ঞাত প্রমাণীকরণ ত্রুটি ঘটেছে।</string> + <string name="error_no_web_browser_found">ব্যবহার করার জন্য একটি ওয়েব ব্রাউজার খুঁজে পাওয়া যায়নি।</string> + <string name="error_failed_app_registration">এই ইনস্ট্যান্স এর সঙ্গে প্রমাণীকরণ ব্যর্থ।</string> + <string name="error_invalid_domain">অবৈধ ডোমেইন প্রবেশ করানো হয়েছে</string> + <string name="error_empty">এই জায়গা খালি হতে পারে না।</string> + <string name="error_network">একটি নেটওয়ার্ক ত্রুটি ঘটেছে! আপনার সংযোগ পরীক্ষা করে আবার চেষ্টা করুন!</string> + <string name="error_generic">একটি ত্রুটি ঘটেছে।</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b>টি পছন্দ</item> + <item quantity="other"><b>%1$s</b>টি পছন্দ</item> + </plurals> + <string name="confirmation_domain_unmuted">%1$s দৃশ্যমান</string> + <string name="notification_subscription_format">%1$s পোস্ট করেছে</string> + <string name="notification_follow_request_format">%1$s তোমাকে ফলো করতে চায়</string> + <string name="notification_follow_format">%1$s তোমাকে ফলো করেছে</string> + <string name="notification_favourite_format">%1$s তোমার টুট বুস্ট করেছে</string> + <string name="notification_reblog_format">%1$s তোমার টুট বুস্ট করেছে</string> + <string name="title_announcements">ঘোষণা</string> + <string name="notification_summary_large">%1$s,%2$s,%3$s এবং %4$d অন্যরা</string> + <string name="notification_subscription_description">যখন আমার সদস্যতা নেওয়া কেউ টুট দেয় তখন বিজ্ঞপ্তি দিবে</string> + <string name="notification_subscription_name">নতুন টুট</string> + <string name="pref_title_animate_custom_emojis">বিশেষ আবেগ বানাও</string> + <string name="pref_title_notification_filter_subscriptions">সদস্যতা আছে এমন একজন টুট দিয়েছে</string> + <string name="no_announcements">কোনো ঘোষণা নেই।</string> + <string name="follow_requests_info">যদিও তোমার অ্যাকাউন্ট রুদ্ধকৃত না, %1$s রা ভেবেছে এই অ্যাকাউন্টগুলোর অনুসরণ অনুরোধ তোমার পরীক্ষা করা উচিত।</string> + <string name="drafts_post_reply_removed">যে টুটের উত্তর খসড়া করেছিলে তা মুছে ফেলা হয়েছে</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">%1$d টার বেশি সংযুক্তি পাঠানো যাবে না।</item> + <item quantity="other">%1$d টার বেশি সংযুক্তি পাঠানো যাবে না।</item> + </plurals> + <string name="drafts_post_failed_to_send">টুট পাঠাতে ব্যর্থ!</string> + <string name="drafts_failed_loading_reply">উত্তরের তথ্য আনতে ব্যর্থ</string> + <string name="account_note_saved">সংরক্ষিত!</string> + <string name="wellbeing_hide_stats_profile">অবতারে পরিসংখ্যান লুকাও</string> + <string name="wellbeing_hide_stats_posts">ছাপার পরিসংখ্যান লুকাও</string> + <string name="limit_notifications">সময়কাল বিজ্ঞপ্তি সীমাবদ্ধ করো</string> + <string name="wellbeing_mode_notice">তোমার মানসিক স্বাস্থে নেতিবাচক প্রভাব ফেলতে পারে এমন জিনিসগুলো লুকানো আছে। যেমন: +\n +\n - পছন্দ/বুস্ট/অনুসরণ বিজ্ঞপ্তি +\n - টুটে পছন্দ/বুস্ট সংখ্যা +\n - অবতারে অনুসরণকারী/পরিসংখ্যান +\n +\nপুশ-বিজ্ঞপ্তিতে প্রভাব পরবে না, কিন্তু বিজ্ঞপ্তি পছন্দ পাল্টাতে পারবে।</string> + <string name="account_note_hint">এই অ্যাকাউন্ট নিয়ে তোমার ব্যক্তিগত লেখা</string> + <string name="pref_title_hide_top_toolbar">শীর্ষস্থানীয় সরঞ্জামের শিরোনামটি লুকাও</string> + <string name="draft_deleted">খসড়া মুছো হয়েছে</string> + <string name="review_notifications">বিজ্ঞপ্তি</string> + <string name="pref_title_wellbeing_mode">সুস্থতা</string> + <string name="duration_indefinite">সময়হীন</string> + <string name="label_duration">সময়কাল</string> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d সেকেন্ড বাকি</item> + <item quantity="other">%1$d সেকেন্ড বাকি</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d মিনিট বাকি</item> + <item quantity="other">%1$d মিনিট বাকি</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d ঘন্টা বাকি</item> + <item quantity="other">%1$d ঘন্টা বাকি</item> + </plurals> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d দিন বাকি</item> + <item quantity="other">%1$d দিন বাকি</item> + </plurals> + <plurals name="poll_info_people"> + <item quantity="one">%1$s জন</item> + <item quantity="other">%1$s জন</item> + </plurals> + <plurals name="poll_info_votes"> + <item quantity="one">%1$sটি ভোট</item> + <item quantity="other">%1$sটি ভোট</item> + </plurals> + <string name="conversation_more_recipients">%1$s, %2$s এবং %3$d আরো অন্য জন</string> + <string name="conversation_2_recipients">%1$s এবং %2$s</string> + <string name="action_subscribe_account">সদস্যতা</string> + <string name="action_unsubscribe_account">সদস্যতা বাতিল</string> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> বুস্ট</item> + <item quantity="other"><b>%1$s</b> বুস্ট</item> + </plurals> + <string name="account_moved_description">%1$s স্থানান্তরিত হয়েছে এখানে:</string> + <string name="post_media_attachments">সংযুক্তি</string> + <string name="post_media_audio">শব্দ</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$dটি নতুন ক্রিয়া</item> + <item quantity="other">%1$dটি নতুন ক্রিয়া</item> + </plurals> + <string name="notification_summary_small">%1$s আর %2$s</string> + <string name="notification_summary_medium">%1$s, %2$s, আর %3$s</string> + <string name="notification_mention_format">%1$s তোমাকে উল্লেখ করেছে</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-bn-rIN/strings.xml b/app/src/main/res/values-bn-rIN/strings.xml new file mode 100644 index 0000000..ed264b3 --- /dev/null +++ b/app/src/main/res/values-bn-rIN/strings.xml @@ -0,0 +1,463 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">একটি ত্রুটি ঘটেছে।</string> + <string name="error_network">একটি নেটওয়ার্ক ত্রুটি ঘটেছে! আপনার সংযোগ পরীক্ষা করে আবার চেষ্টা করুন!</string> + <string name="error_empty">এই জায়গা খালি হতে পারে না।</string> + <string name="error_invalid_domain">অবৈধ ডোমেইন প্রবেশ করানো হয়েছে</string> + <string name="error_failed_app_registration">এই ইনস্ট্যান্স এর সঙ্গে প্রমাণীকরণ ব্যর্থ।</string> + <string name="error_no_web_browser_found">ব্যবহার করার জন্য একটি ওয়েব ব্রাউজার খুঁজে পাওয়া যায়নি।</string> + <string name="error_authorization_unknown">একটি অজ্ঞাত প্রমাণীকরণ ত্রুটি ঘটেছে।</string> + <string name="error_authorization_denied">অনুমোদন অস্বীকার করা হয়েছে।</string> + <string name="error_retrieving_oauth_token">একটি লগইন টোকেন পেতে ব্যর্থ।</string> + <string name="error_compose_character_limit">এই স্টেটাস টি খুব দীর্ঘ!</string> + <string name="error_media_upload_type">যে ধরনের ফাইল আপলোড করা যাবে না।</string> + <string name="error_media_upload_opening">ওই ফাইল খোলা যাবে না।</string> + <string name="error_media_upload_permission">মিডিয়া পড়তে অনুমতি প্রয়োজন।</string> + <string name="error_media_download_permission">মিডিয়া সংরক্ষণ করার অনুমতি প্রয়োজন।</string> + <string name="error_media_upload_image_or_video">ছবি এবং ভিডিও উভয় একই স্টেটাস থেকে সংযুক্ত করা যাবে না।</string> + <string name="error_media_upload_sending">আপলোড করতে ব্যর্থ হয়েছে।</string> + <string name="error_sender_account_gone">টুট পাঠাতে গিয়ে একটি ত্রুটি ঘটেছে।</string> + <string name="title_home">ঘর</string> + <string name="title_notifications">বিজ্ঞপ্তিগুলি</string> + <string name="title_public_local">স্থানীয়</string> + <string name="title_public_federated">ফেডারেটেড</string> + <string name="title_direct_messages">সরাসরি বার্তা</string> + <string name="title_tab_preferences">ট্যাবগুলি</string> + <string name="title_view_thread">টুট</string> + <string name="title_posts">পোস্টগুলি</string> + <string name="title_posts_with_replies">উত্তরের সাথে</string> + <string name="title_posts_pinned">পিন করা</string> + <string name="title_follows">অনুসরণ</string> + <string name="title_followers">অনুগামিবৃন্দ</string> + <string name="title_favourites">প্রিয়গুলো</string> + <string name="title_mutes">শব্দ বন্ধ ব্যবহারকারী</string> + <string name="title_blocks">অবরুদ্ধ ব্যবহারকারী</string> + <string name="title_follow_requests">অনুরোধ অনুসরণ করুন</string> + <string name="title_edit_profile">আপনার প্রোফাইল সম্পাদনা করুন</string> + <string name="title_drafts">খসড়াগুলো</string> + <string name="title_licenses">লাইসেন্সগুলি</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s সমর্থন দিয়েছে</string> + <string name="post_sensitive_media_title">সংবেদনশীল কন্টেন্ট</string> + <string name="post_media_hidden_title">মিডিয়া লুকানো</string> + <string name="post_sensitive_media_directions">দেখার জন্য ক্লিক করুন</string> + <string name="post_content_warning_show_more">আরও দেখাও</string> + <string name="post_content_warning_show_less">প্রদর্শন কম</string> + <string name="post_content_show_more">বিস্তৃত</string> + <string name="post_content_show_less">বন্ধ</string> + <string name="message_empty">এখানে কিছুই নেই।</string> + <string name="footer_empty">এখানে কিছু নেই. রিফ্রেশ করতে নিচে টানুন!</string> + <string name="notification_reblog_format">%1$s সমর্থন দিয়েছে</string> + <string name="notification_favourite_format">%1$s আপনার টুট পছন্দ করেছে</string> + <string name="notification_follow_format">%1$s আপনাকে অনুসরণ করেছেন</string> + <string name="report_username_format">রিপোর্ট @%1$s</string> + <string name="report_comment_hint">অতিরিক্ত মন্তব্যগুলি\?</string> + <string name="action_quick_reply">দ্রুত উত্তর</string> + <string name="action_reply">উত্তর</string> + <string name="action_reblog">সমর্থন</string> + <string name="action_unreblog">সমর্থন সরান</string> + <string name="action_favourite">পছন্দ</string> + <string name="action_unfavourite">পছন্দ সরান</string> + <string name="action_more">অধিক</string> + <string name="action_compose">রচনা</string> + <string name="action_login">মাস্টোডনের সঙ্গে লগইন করো</string> + <string name="action_logout">লগ আউট</string> + <string name="action_logout_confirm">আপনি কি অ্যাকাউন্ট %1$s থেকে লগ আউট করতে চান\?</string> + <string name="action_follow">অনুসরণ কর</string> + <string name="action_unfollow">অনুসরণ করা বন্ধ করুন</string> + <string name="action_block">অবরুদ্ধ</string> + <string name="action_unblock">অবরোধ মুক্ত</string> + <string name="action_hide_reblogs">সমর্থন লুকান</string> + <string name="action_show_reblogs">সমর্থন দেখান</string> + <string name="action_report">রিপোর্ট</string> + <string name="action_delete">মুছে ফেল</string> + <string name="action_delete_and_redraft">মুছুন এবং পুনরায় ড্রাফট কর</string> + <string name="action_send">টুট</string> + <string name="action_send_public">টুট!</string> + <string name="action_retry">পুনরায় চেষ্টা করা</string> + <string name="action_close">বদ্ধ</string> + <string name="action_view_profile">প্রোফাইল</string> + <string name="action_view_preferences">পছন্দসমূহ</string> + <string name="action_view_account_preferences">অ্যাকাউন্টের পছন্দসমূহ</string> + <string name="action_view_favourites">প্রিয়গুলো</string> + <string name="action_view_mutes">শব্দ বন্ধ ব্যবহারকারী</string> + <string name="action_view_blocks">অবরুদ্ধ ব্যবহারকারী</string> + <string name="action_view_follow_requests">অনুরোধ অনুসরণ করুন</string> + <string name="action_view_media">মিডিয়া</string> + <string name="action_open_in_web">ব্রাউজারে খোলা</string> + <string name="action_add_media">মিডিয়া যোগ করুন</string> + <string name="action_photo_take">ছবি তোল</string> + <string name="action_share">ভাগ</string> + <string name="action_mute">শব্দ বন্ধ</string> + <string name="action_unmute">সশব্দ</string> + <string name="action_mention">উল্লেখ</string> + <string name="action_hide_media">মিডিয়া লুকান</string> + <string name="action_open_drawer">ড্রয়ার খুলুন</string> + <string name="action_save">সংরক্ষণ</string> + <string name="action_edit_profile">প্রোফাইল সম্পাদনা করো</string> + <string name="action_edit_own_profile">সম্পাদন</string> + <string name="action_undo">বাতিল</string> + <string name="action_accept">গ্রহণ</string> + <string name="action_reject">প্রত্যাখ্যান</string> + <string name="action_search">অনুসন্ধান</string> + <string name="action_access_drafts">খসড়াগুলো</string> + <string name="action_toggle_visibility">টুট দৃশ্যমানতা</string> + <string name="action_content_warning">সতর্কবার্তা</string> + <string name="action_emoji_keyboard">ইমোজি কীবোর্ড</string> + <string name="action_add_tab">ট্যাব যুক্ত করুন</string> + <string name="action_links">লিংকসমূহ</string> + <string name="action_mentions">উল্লেখসমূহ</string> + <string name="action_hashtags">হ্যাশট্যাগ</string> + <string name="action_open_reblogger">সমর্থক লেখক খুলুন</string> + <string name="action_open_reblogged_by">সমর্থন দেখান</string> + <string name="action_open_faved_by">প্রিয়গুলি দেখান</string> + <string name="title_hashtags_dialog">হ্যাশট্যাগ</string> + <string name="title_mentions_dialog">উল্লেখসমূহ</string> + <string name="title_links_dialog">লিংকসমূহ</string> + <string name="action_open_media_n">মিডিয়া খুলুন #%1$d</string> + <string name="download_image">%1$s ডাউনলোড হচ্ছে</string> + <string name="action_copy_link">লিঙ্ক অনুলিপি করুন</string> + <string name="action_open_as">%1$s হিসাবে খুলুন</string> + <string name="action_share_as">হিসাবে ভাগ করুন …</string> + <string name="download_media">মিডিয়া ডাউনলোড করুন</string> + <string name="downloading_media">মিডিয়া ডাউনলোড করা হচ্ছে</string> + <string name="send_post_link_to">টুট URL ভাগ করুন …</string> + <string name="send_post_content_to">টুট ভাগ করুন …</string> + <string name="send_media_to">মিডিয়া শেয়ার করুন …</string> + <string name="confirmation_reported">পাঠানো হয়েছে!</string> + <string name="confirmation_unblocked">ব্যবহারকারী অবরোধ মুক্ত</string> + <string name="confirmation_unmuted">ব্যবহারকারী সশব্দ</string> + <string name="hint_domain">কোন ইনস্ট্যান্স\?</string> + <string name="hint_compose">কি হচ্ছে\?</string> + <string name="hint_content_warning">সতর্কবার্তা</string> + <string name="hint_display_name">প্রদর্শন নাম</string> + <string name="hint_note">জীবনী</string> + <string name="hint_search">অনুসন্ধান…</string> + <string name="search_no_results">কোন ফলাফল নেই</string> + <string name="label_quick_reply">উত্তর…</string> + <string name="label_avatar">অবতার</string> + <string name="label_header">হেডার</string> + <string name="link_whats_an_instance">ইনস্ট্যান্স কি\?</string> + <string name="login_connection">সংযুক্ত হচ্ছে …</string> + <string name="dialog_whats_an_instance">কোনও উদাহরণের ঠিকানা বা ডোমেন এখানে প্রবেশ করা যেতে পারে যেমন mastodon.social, icosahedron.website, social.tchncs.de, এবং <a href="https://instances.social"> আরও! </a> +\n +\nআপনার যদি এখনো অ্যাকাউন্ট না থাকে তবে আপনি যে ইনস্ট্যান্সটিতে যোগ দিতে চান সেটির নামটি প্রবেশ করতে এবং সেখানে একটি অ্যাকাউন্ট তৈরি করতে পারেন। +\n +\nএকটি ইনস্ট্যান্স একটি একক স্থান যেখানে আপনার অ্যাকাউন্ট হোস্ট করা হয়, তবে আপনি সহজেই যোগাযোগ করতে পারেন এবং অন্যান্য ক্ষেত্রে যেমন আপনি একই সাইটে ছিলেন তা অনুসরণ করতে পারেন। +\n +\nআরো তথ্য <a href="https://joinmastodon.org"> joinmastodon.org </a> এ পাওয়া যেতে পারে। </string> + <string name="dialog_title_finishing_media_upload">মিডিয়া আপলোড সমাপ্ত করা হচ্ছে</string> + <string name="dialog_message_uploading_media">আপলোড হচ্ছে …</string> + <string name="dialog_download_image">ডাউনলোড</string> + <string name="dialog_message_cancel_follow_request">অনুসরণ অনুরোধ প্রত্যাহার\?</string> + <string name="dialog_unfollow_warning">এই অ্যাকাউন্টটি অনুসরণ করবেন না\?</string> + <string name="dialog_delete_post_warning">এই টুট টি মুছে ফেলবেন\?</string> + <string name="dialog_redraft_post_warning">এই টুট টি মুছে ফেলবেন এবং পুনরায় ড্রাফট করবেন\?</string> + <string name="visibility_public">সর্বজনীন: পাবলিক টাইমলাইনে পোস্ট কর</string> + <string name="visibility_unlisted">তালিকাভুক্ত নয়: সর্বজনীন সময়সূচীগুলিতে দেখাবেন না</string> + <string name="visibility_private">শুধুমাত্র অনুসরণকারীদের: শুধুমাত্র অনুসরণকারীদের পোস্ট করুন</string> + <string name="visibility_direct">সরাসরি: শুধুমাত্র উল্লেখ ব্যবহারকারীদের পোস্ট করুন</string> + <string name="pref_title_edit_notification_settings">বিজ্ঞপ্তিগুলি</string> + <string name="pref_title_notifications_enabled">বিজ্ঞপ্তিগুলি</string> + <string name="pref_title_notification_alerts">সতর্কতা</string> + <string name="pref_title_notification_alert_sound">একটি শব্দ সঙ্গে অবহিত</string> + <string name="pref_title_notification_alert_vibrate">কম্পন সঙ্গে অবহিত</string> + <string name="pref_title_notification_alert_light">আলো সঙ্গে অবহিত</string> + <string name="pref_title_notification_filters">যখন আমাকে অবহিত</string> + <string name="pref_title_notification_filter_mentions">উল্লিখিত</string> + <string name="pref_title_notification_filter_follows">অনুসরণ</string> + <string name="pref_title_notification_filter_reblogs">আমার পোস্টগুলির সমর্থন হচ্ছে</string> + <string name="pref_title_notification_filter_favourites">আমার পোস্টগুলির পছন্দ হচ্ছে</string> + <string name="pref_title_notification_filter_poll">নির্বাচন শেষ হয়েছে</string> + <string name="pref_title_appearance_settings">উপস্থিতি</string> + <string name="pref_title_app_theme">এপ্লিকেশন এর থিম</string> + <string name="pref_title_timelines">টাইমলাইন</string> + <string name="pref_title_timeline_filters">ফিল্টার</string> + <string name="app_them_dark">অন্ধকার</string> + <string name="app_theme_light">আলো</string> + <string name="app_theme_black">কালো</string> + <string name="app_theme_auto">সূর্যাস্ত স্বয়ংক্রিয়</string> + <string name="app_theme_system">সিস্টেম ডিজাইন ব্যবহার করুন</string> + <string name="pref_title_browser_settings">ব্রাউজার</string> + <string name="pref_title_custom_tabs">ক্রোম কাস্টম ট্যাব ব্যবহার করুন</string> + <string name="pref_title_language">ভাষা</string> + <string name="pref_title_bot_overlay">বট জন্য সূচক প্রদর্শন করুন</string> + <string name="pref_title_animate_gif_avatars">GIF অবতার অ্যানিমেশন করুন</string> + <string name="pref_title_post_filter">টাইমলাইন ফিল্টারিং</string> + <string name="pref_title_post_tabs">ট্যাবগুলি</string> + <string name="pref_title_show_boosts">সমর্থন দেখান</string> + <string name="pref_title_show_replies">উত্তর প্রদর্শন করুন</string> + <string name="pref_title_show_media_preview">মিডিয়া পূর্বরূপ ডাউনলোড করুন</string> + <string name="pref_title_proxy_settings">প্রক্সি</string> + <string name="pref_title_http_proxy_settings">HTTP প্রক্সি</string> + <string name="pref_title_http_proxy_enable">HTTP প্রক্সি সক্ষম কর</string> + <string name="pref_title_http_proxy_server">HTTP প্রক্সি সার্ভার</string> + <string name="pref_title_http_proxy_port">HTTP প্রক্সি পোর্ট</string> + <string name="pref_default_post_privacy">ডিফল্ট পোস্ট গোপনীয়তা</string> + <string name="pref_default_media_sensitivity">সর্বদা সংবেদনশীল হিসাবে মিডিয়া চিহ্নিত করুন</string> + <string name="pref_publishing">প্রকাশনা (সার্ভারের সাথে সিঙ্ক করা)</string> + <string name="pref_failed_to_sync">সেটিংস সিঙ্ক করতে ব্যর্থ</string> + <string name="post_privacy_public">সর্বজনীন</string> + <string name="post_privacy_unlisted">অতালিকাভুক্ত</string> + <string name="post_privacy_followers_only">শুধুমাত্র অনুগামিবৃন্দ</string> + <string name="pref_post_text_size">স্থিতি টেক্সট আকার</string> + <string name="post_text_size_smallest">কনিষ্ঠ</string> + <string name="post_text_size_small">ছোট</string> + <string name="post_text_size_medium">মাঝারি</string> + <string name="post_text_size_large">বড়</string> + <string name="post_text_size_largest">বৃহত্তম</string> + <string name="notification_mention_name">নতুন উল্লেখসমূহ</string> + <string name="notification_mention_descriptions">নতুন উল্লেখ সম্পর্কে বিজ্ঞপ্তি</string> + <string name="notification_follow_name">নতুন অনুসরণকারী</string> + <string name="notification_follow_description">নতুন অনুসরণকারী সম্পর্কে বিজ্ঞপ্তি</string> + <string name="notification_boost_name">সমর্থনগুলি</string> + <string name="notification_boost_description">আপনার টুটগুলি সমর্থন হলে বিজ্ঞপ্তিগুলি</string> + <string name="notification_favourite_name">প্রিয়গুলো</string> + <string name="notification_favourite_description">আপনার টুটগুলি প্রিয় হিসাবে চিহ্নিত পেতে বিজ্ঞপ্তি</string> + <string name="notification_poll_name">নির্বাচনগুলি</string> + <string name="notification_poll_description">শেষ হয়েছে যে নির্বাচনগুলির সম্পর্কে বিজ্ঞপ্তি</string> + <string name="notification_mention_format">%1$s আপনাকে উল্লিখিত করেছে</string> + <string name="notification_summary_large">%1$s,%2$s,%3$s এবং %4$d আরো অন্য জন</string> + <string name="notification_summary_medium">%1$s, %2$s, আর %3$s</string> + <string name="notification_summary_small">%1$s আর %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="other">%1$d নতুন মিথস্ক্রিয়া</item> + </plurals> + <string name="description_account_locked">লক অ্যাকাউন্ট</string> + <string name="about_title_activity">সম্পর্কিত</string> + <string name="about_tusky_version">টাস্কি %1$s</string> + <string name="about_tusky_license">টাস্কি মুক্ত এবং ওপেন সোর্স সফ্টওয়্যার। এটি GNU জেনারেল পাবলিক লাইসেন্স সংস্করণ 3 এর অধীনে লাইসেন্সযুক্ত। আপনি এখানে লাইসেন্স দেখতে পারেন: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">প্রকল্প ওয়েবসাইট: +\nhttps://tusky.app</string> + <string name="about_bug_feature_request_site">বাগ রিপোর্ট এবং বৈশিষ্ট্য অনুরোধ: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">টাস্কির প্রোফাইল</string> + <string name="post_share_content">টুট এর কন্টেন্ট ভাগ করুন</string> + <string name="post_share_link">টুট এর সাথে লিংক ভাগ করুন</string> + <string name="post_media_images">চিত্রগুলি</string> + <string name="post_media_video">ভিডিও</string> + <string name="state_follow_requested">অনুরোধ অনুসরণ করুন</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">%1$dy এ</string> + <string name="abbreviated_in_days">%1$dd এ</string> + <string name="abbreviated_in_hours">%1$dh এ</string> + <string name="abbreviated_in_minutes">%1$dm এ</string> + <string name="abbreviated_in_seconds">%1$ds এ</string> + <string name="abbreviated_years_ago">%1$dy</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="follows_you">আপনাকে অনুসরন করে</string> + <string name="pref_title_alway_show_sensitive_media">সর্বদা সংবেদনশীল কন্টেন্ট প্রদর্শন করুন</string> + <string name="title_media">মিডিয়া</string> + <string name="replying_to">\@%1$s কে উত্তর দিচ্ছে</string> + <string name="load_more_placeholder_text">আরো লোড কর</string> + <string name="pref_title_public_filter_keywords">পাবলিক টাইমলাইন</string> + <string name="pref_title_thread_filter_keywords">কথাবার্তা</string> + <string name="filter_addition_title">ফিল্টার যোগ করুন</string> + <string name="filter_edit_title">ফিল্টার সম্পাদনা করুন</string> + <string name="filter_dialog_remove_button">সরাও</string> + <string name="filter_dialog_update_button">আপডেট</string> + <string name="filter_add_description">বাক্য ফিল্টার কর</string> + <string name="add_account_name">অ্যাকাউন্ট যোগ করুন</string> + <string name="add_account_description">নতুন ম্যাস্টোডোন অ্যাকাউন্ট যোগ করুন</string> + <string name="action_lists">তালিকাসমূহ</string> + <string name="title_lists">তালিকাসমূহ</string> + <string name="error_create_list">তালিকা তৈরি করা যায়নি</string> + <string name="error_rename_list">তালিকা নামকরণ করা যায়নি</string> + <string name="error_delete_list">তালিকা মুছে ফেলা যায়নি</string> + <string name="action_create_list">একটি তালিকা তৈরি করুন</string> + <string name="action_rename_list">তালিকা পুনঃ নামকরণ কর</string> + <string name="action_delete_list">তালিকা মুছে দিন</string> + <string name="hint_search_people_list">আপনি অনুসরণ মানুষের জন্য অনুসন্ধান করুন</string> + <string name="action_add_to_list">তালিকায় অ্যাকাউন্ট যোগ করুন</string> + <string name="action_remove_from_list">তালিকা থেকে অ্যাকাউন্ট সরান</string> + <string name="compose_active_account_description">অ্যাকাউন্ট %1$s থেকে পোস্ট করা হচ্ছে</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">দৃষ্টি প্রতিবন্ধী জন্য বর্ণনা করুন +\n(%1$d অক্ষর সীমা)</item> + </plurals> + <string name="action_set_caption">ক্যাপশন সেট করুন</string> + <string name="action_remove">সরান</string> + <string name="lock_account_label">অ্যাকাউন্ট লক করুন</string> + <string name="lock_account_label_description">অনুসারী অনুমোদন করার জন্য আপনাকে প্রয়োজন</string> + <string name="compose_save_draft">ড্রাফট সংরক্ষণ\?</string> + <string name="send_post_notification_title">টুট পাঠানো হচ্ছে …</string> + <string name="send_post_notification_error_title">টুট পাঠাতে গিয়ে একটি ত্রুটি ঘটেছে</string> + <string name="send_post_notification_channel_name">টুট পাঠানো হচ্ছে</string> + <string name="send_post_notification_cancel_title">পাঠানো বাতিল</string> + <string name="send_post_notification_saved_content">টুট এর একটি কপি আপনার ড্রাফটে সংরক্ষণ করা হয়েছে</string> + <string name="action_compose_shortcut">রচনা</string> + <string name="error_no_custom_emojis">আপনার ইনস্ট্যান্স %1$s এর কোনো কাস্টম ইমোজিস নেই</string> + <string name="emoji_style">ইমোজি স্টাইল</string> + <string name="system_default">সিস্টেমের ডিফল্ট</string> + <string name="download_fonts">আপনাকে প্রথমে এই ইমোজি সেটগুলি ডাউনলোড করতে হবে</string> + <string name="performing_lookup_title">অনুসন্ধান করা হচ্ছে …</string> + <string name="expand_collapse_all_posts">সমস্ত স্টেটাস প্রসারিত/সংকুচিত করুন</string> + <string name="action_open_post">টুট খুলুন</string> + <string name="restart_required">অ্যাপ্লিকেশন পুনরায় আরম্ভ করা প্রয়োজন</string> + <string name="restart_emoji">এই পরিবর্তনগুলি প্রয়োগ করার জন্য আপনাকে টাস্কি পুনরায় চালু করতে হবে</string> + <string name="later">পরবর্তীতে</string> + <string name="restart">পুনরারম্ভ</string> + <string name="caption_systememoji">আপনার ডিভাইসের ডিফল্ট ইমোজি সেট</string> + <string name="caption_blobmoji">অ্যান্ড্রয়েড ৪.৪-৭.১ থেকে পরিচিত ব্লোব ইমোজিস</string> + <string name="caption_twemoji">ম্যাস্টোডোন এর মান ইমোজি সেট</string> + <string name="caption_notoemoji">গুগল এর বর্তমান ইমোজি সেট</string> + <string name="download_failed">ডাউনলোড ব্যর্থ হয়েছে</string> + <string name="profile_badge_bot_text">বট</string> + <string name="account_moved_description">%1$s স্থানান্তরিত করেছে এতে:</string> + <string name="reblog_private">মূল শ্রোতাদেড় সমথন পাঠাও</string> + <string name="unreblog_private">সমর্থন ফেরত</string> + <string name="license_description">টাস্কি নিম্নলিখিত ওপেন সোর্স প্রকল্প থেকে কোড এবং সম্পদ রয়েছে:</string> + <string name="license_apache_2">এপাচে লাইসেন্সের অধীনে লাইসেন্স (নীচের কপি)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">প্রোফাইল মেটাডেটা</string> + <string name="profile_metadata_add">তথ্য যোগ করুন</string> + <string name="profile_metadata_label_label">লেবেল</string> + <string name="profile_metadata_content_label">উপাদান</string> + <string name="pref_title_absolute_time">পরম সময় ব্যবহার করুন</string> + <string name="label_remote_account">নীচের তথ্য অসম্পূর্ণভাবে ব্যবহারকারীর প্রোফাইল প্রতিফলিত হতে পারে। ব্রাউজারে সম্পূর্ণ প্রোফাইল খুলতে টিপুন।</string> + <string name="unpin_action">আনপিন</string> + <string name="pin_action">পিন</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> প্রিয়</item> + <item quantity="other"><b>%1$s</b> পছন্দসই</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> বুস্ট</item> + <item quantity="other"><b>%1$s</b> বুস্ট</item> + </plurals> + <string name="title_reblogged_by">দ্বারা সর্মথন</string> + <string name="title_favourited_by">দ্বারা পছন্দ</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s এবং %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s এবং %3$d আরো অন্য জন</string> + <string name="description_post_media">মিডিয়া: %1$s</string> + <string name="description_post_cw">সতর্কবার্তা: %1$s</string> + <string name="description_post_media_no_description_placeholder">বর্ণনা নাই</string> + <string name="description_post_reblogged">আবার ব্লগ</string> + <string name="description_post_favourited">পছন্দ</string> + <string name="description_visibility_public">সর্বজনীন</string> + <string name="description_visibility_unlisted">অতালিকাভুক্ত</string> + <string name="description_visibility_private">অনুগামিবৃন্দ</string> + <string name="description_visibility_direct">সরাসরি</string> + <string name="description_poll">পছন্দগুলি সহ নর্বাচন: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="hint_list_name">নামের তালিকা</string> + <string name="edit_hashtag_hint"># ছাড়া হ্যাশট্যাগ</string> + <string name="notifications_clear">পরিষ্কার</string> + <string name="notifications_apply_filter">ফিল্টার</string> + <string name="filter_apply">প্রয়োগ</string> + <string name="compose_shortcut_long_label">টুট রচনা করুন</string> + <string name="compose_shortcut_short_label">রচনা</string> + <string name="notification_clear_text">আপনি কি আপনার সমস্ত বিজ্ঞপ্তি স্থায়ীভাবে মুছে ফেলতে চান\?</string> + <string name="compose_preview_image_description">ছবি %1$s এর জন্য ক্রিয়া</string> + <string name="poll_info_format"> <!-- ১৫ ভোট • ১ ঘন্টা বাকি --> %1$s • %2$s</string> + <string name="poll_info_time_absolute">%1$s এ শেষ হবে</string> + <string name="poll_info_closed">বন্ধ</string> + <string name="poll_vote">ভোট</string> + <string name="poll_ended_voted">আপনি ভোট দিয়েছেন যে নির্বাচন এ সেটি শেষ হয়েছে</string> + <string name="poll_ended_created">আপনি তৈরি একটি নির্বাচন শেষ হয়েছে</string> + <string name="button_continue">চালিয়ে যাও</string> + <string name="button_back">পিছাও</string> + <string name="button_done">সম্পন্ন</string> + <string name="report_sent_success">%1$s এ সফলভাবে রিপোর্ট করা হয়েছে</string> + <string name="hint_additional_info">অতিরিক্ত মন্তব্যগুলি</string> + <string name="report_remote_instance">%1$s এ ফরওয়ার্ড করুন</string> + <string name="failed_report">রিপোর্ট করতে ব্যর্থ হয়েছে</string> + <string name="failed_fetch_posts">স্টেটাসগুলি আনতে ব্যর্থ</string> + <string name="report_description_1">রিপোর্ট আপনার সার্ভার মডারেটরে পাঠানো হবে। আপনি নীচের এই অ্যাকাউন্টটি কেন প্রতিবেদন করছেন তা ব্যাখ্যা করতে পারেন:</string> + <string name="report_description_remote_instance">এই একাউন্ট তা একটি অন্য সার্ভারের। রিপোর্ট এর সঙ্গে একটি বেনামি কপি ওখানে পাঠাতে চান\?</string> + <string name="title_domain_mutes">গোপন ডোমেইনগুলি</string> + <string name="action_view_domain_mutes">গোপন ডোমেইনগুলি</string> + <string name="action_mute_domain">নিঃশব্দ %1$s</string> + <string name="confirmation_domain_unmuted">%1$s লুকোনো না</string> + <string name="mute_domain_warning">আপনি কি সব %1$s ব্লক করতে চান\? আপনি যে ডোমেন থেকে কোনও পাবলিক টাইমলাইনে বা আপনার বিজ্ঞপ্তিগুলিতে সামগ্রী দেখতে পাবেন না। আপনার অনুসরণকারীদের সরানো হবে।</string> + <string name="mute_domain_warning_dialog_ok">পুরো ডোমেইন লুকান</string> + <string name="filter_dialog_whole_word">সম্পূর্ণ শব্দ</string> + <string name="filter_dialog_whole_word_description">যখন শব্দ বা বাক্যাংশটি শুধুমাত্র আলফানিউমেরিক হয় তখন এটি শুধুমাত্র তখনই প্রয়োগ করা হবে যদি এটি সম্পূর্ণ শব্দটির সাথে মেলে</string> + <string name="pref_title_alway_open_spoiler">সর্বদা সামগ্রী সতর্কতা সহ চিহ্নিত টুটগুলি প্রসারিত করুন</string> + <string name="title_accounts">অক্কোউন্টগুলি</string> + <string name="failed_search">অনুসন্ধান করতে ব্যর্থ</string> + <string name="action_add_poll">পোল যুক্ত করুন</string> + <string name="create_poll_title">ভোটগ্রহণ</string> + <string name="duration_5_min">৫ মিনিট</string> + <string name="duration_30_min">৩০ মিনিট</string> + <string name="duration_1_hour">১ ঘন্টা</string> + <string name="duration_6_hours">৬ ঘন্টা</string> + <string name="duration_1_day">১ দিন</string> + <string name="duration_3_days">৩ দিন</string> + <string name="duration_7_days">৭ দিন</string> + <string name="add_poll_choice">পছন্দ যুক্ত করুন</string> + <string name="poll_allow_multiple_choices">একাধিক পছন্দ</string> + <string name="poll_new_choice_hint">পছন্দ %1$d</string> + <string name="edit_poll">সম্পাদন</string> + <string name="title_scheduled_posts">নির্ধারিত টুটগুলি</string> + <string name="action_edit">সম্পাদন</string> + <string name="action_access_scheduled_posts">নির্ধারিত টুটগুলি</string> + <string name="action_schedule_post">নির্ধারিত টুট</string> + <string name="action_reset_schedule">রিসেট</string> + <string name="about_powered_by_tusky">টাস্কি দ্বারা চালিত</string> + <string name="post_lookup_error_format">%1$s পোস্ট অনুসন্ধানে ত্রুটি</string> + <string name="action_view_bookmarks">বুকমার্কগুলি</string> + <string name="action_bookmark">বুকমার্ক</string> + <string name="notification_follow_request_format">%1$s আপনাকে অনুসরণ করার জন্য অনুরোধ করেছে</string> + <string name="title_bookmarks">বুকমার্কগুলি</string> + <string name="pref_title_notification_filter_follow_requests">অনুরোধ অনুসরণ করো</string> + <string name="dialog_mute_hide_notifications">বিজ্ঞপ্তি লুকাও</string> + <string name="dialog_mute_warning">নিঃশব্দ @%1$s\?</string> + <string name="dialog_block_warning">অবরুদ্ধ @%1$s\?</string> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d দিন বাকি</item> + <item quantity="other">%1$d দিন বাকি</item> + </plurals> + <string name="pref_title_gradient_for_media">লুকানো মিডিয়ার জন্য রঙিন গ্রেডিয়েন্ট ব্যবহার করি</string> + <string name="action_unmute_conversation">আলাপ বন্ধ করো</string> + <string name="action_mute_conversation">আলাপ বন্ধ করো</string> + <string name="action_unmute_domain">আনমিউট %1$s</string> + <string name="action_unmute_desc">আনমিউট %1$s</string> + <string name="notification_follow_request_name">অনুরোধ অনুসরণ করুন</string> + <string name="pref_title_confirm_reblogs">বুস্ট করার আগে নিশ্চিত করো</string> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d সেকেন্ড বাকি</item> + <item quantity="other">%1$d সেকেন্ড বাকি</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d মিনিট বাকি</item> + <item quantity="other">%1$d মিনিট বাকি</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d ঘন্টা বাকি</item> + <item quantity="other">%1$d ঘন্টা বাকি</item> + </plurals> + <plurals name="poll_info_people"> + <item quantity="one">%1$s জন</item> + <item quantity="other">%1$s জন</item> + </plurals> + <plurals name="poll_info_votes"> + <item quantity="one">%1$sটি ভোট</item> + <item quantity="other">%1$sটি ভোট</item> + </plurals> + <string name="list">তালিকা</string> + <string name="select_list_title">তালিকা নির্বাচন করো</string> + <string name="hashtags">হ্যাশট্যাগ</string> + <string name="add_hashtag_title">হ্যাশট্যাগ যোগ করো</string> + <string name="description_post_bookmarked">বুকমার্ককৃত</string> + <string name="notification_follow_request_description">অনুসরণ রিকোয়েস্টের বিজ্ঞপ্তি</string> + <string name="pref_main_nav_position_option_bottom">সবচেয়ে শেষ</string> + <string name="pref_main_nav_position_option_top">সর্বপ্রথম</string> + <string name="pref_main_nav_position">মূল ন্যাভিগেশন জায়গা</string> + <string name="pref_title_enable_swipe_for_tabs">ট্যাবের মাঝে সোয়াইপ সংকেত চালু করো</string> + <string name="pref_title_show_cards_in_timelines">টাইমলাইনে লিঙ্ক প্রিভিউ দেখাও</string> + <string name="no_scheduled_posts">তোমার কোনো সময়সূচীত স্ট্যাটাস নেই।</string> + <string name="no_drafts">তোমার কোনো খসড়া নেই।</string> + <string name="warning_scheduling_interval">মাস্টোডনের সর্বনিম্ন ৫ মিনিটের সময়সূচীর বিরতি আছে।</string> + <string name="pref_title_hide_top_toolbar">শীর্ষস্থানীয় সরঞ্জামদণ্ডের শিরোনামটি লুকান</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-ca/strings.xml b/app/src/main/res/values-ca/strings.xml new file mode 100644 index 0000000..138c448 --- /dev/null +++ b/app/src/main/res/values-ca/strings.xml @@ -0,0 +1,629 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">S\'ha produït un error.</string> + <string name="error_empty">Això no pot estar buit.</string> + <string name="error_invalid_domain">El domini que s\'ha introduït no és vàlid</string> + <string name="error_failed_app_registration">No s\'ha pogut autenticar amb aquesta instància. Si això continua, proveu d\'iniciar sessió al navegador des del menú.</string> + <string name="error_no_web_browser_found">No s\'ha trobat cap navegador web per a utilitzar.</string> + <string name="error_authorization_unknown">S\'ha produït un error d\'autorització no identificat. Si això continua, proveu d\'iniciar sessió al navegador des del menú.</string> + <string name="error_authorization_denied">S\'ha denegat l\'autorització. Si esteu segur que heu proporcionat les credencials correctes, proveu d\'iniciar sessió al navegador des del menú.</string> + <string name="error_retrieving_oauth_token">No s\'ha pogut obtenir el token d\'inici de sessió. Si això continua, proveu d\'iniciar sessió al navegador des del menú.</string> + <string name="error_compose_character_limit">La publicació és massa llarga!</string> + <string name="error_media_upload_type">No es pot pujar aquest tipus de fitxer.</string> + <string name="error_media_upload_opening">No es pot obrir aquest tipus de fitxer.</string> + <string name="error_media_upload_permission">Cal permís d\'accés a l\'emmagatzematge.</string> + <string name="error_media_download_permission">Cal permís d\'escriptura a l\'emmagatzematge.</string> + <string name="error_media_upload_image_or_video">Les imatges i els vídeos no es poden adjuntar a la mateixa publicació.</string> + <string name="error_media_upload_sending">Ha fallat la pujada.</string> + <string name="title_home">Inici</string> + <string name="title_notifications">Notificacions</string> + <string name="title_public_local">Local</string> + <string name="title_public_federated">Federació</string> + <string name="title_view_thread">Fil</string> + <string name="title_posts">Publicacions</string> + <string name="title_follows">Seguits</string> + <string name="title_followers">Seguidors</string> + <string name="title_favourites">Preferits</string> + <string name="title_mutes">Usuaris silenciats</string> + <string name="title_blocks">Usuaris blocats</string> + <string name="title_follow_requests">Peticions de seguiment</string> + <string name="title_edit_profile">Edita el perfil</string> + <string name="title_drafts">Esborranys</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s ha impulsat</string> + <string name="post_sensitive_media_title">Contingut sensible</string> + <string name="post_sensitive_media_directions">Fes clic per a visualitzar-lo</string> + <string name="post_content_warning_show_more">Mostra\'n més</string> + <string name="post_content_warning_show_less">Mostra\'n menys</string> + <string name="footer_empty">No hi res aquí. Llisca avall per a actualitzar!</string> + <string name="notification_reblog_format">%1$s ha impulsat la teva publicació</string> + <string name="notification_favourite_format">%1$s ha marcat com a preferida la teva publicació</string> + <string name="notification_follow_format">%1$s et segueix</string> + <string name="report_username_format">Denuncia @%1$s</string> + <string name="report_comment_hint">Cap comentari addicional?</string> + <string name="action_reply">Respon</string> + <string name="action_reblog">Impulsa</string> + <string name="action_favourite">Preferit</string> + <string name="action_more">Més</string> + <string name="action_compose">Escriure</string> + <string name="action_login">Inicia sessió amb Tusky</string> + <string name="action_logout">Tanca sessió</string> + <string name="action_follow">Segueix</string> + <string name="action_unfollow">Deixa de seguir</string> + <string name="action_block">Bloca</string> + <string name="action_unblock">Deixa de blocar</string> + <string name="action_report">Denuncia</string> + <string name="action_delete">Elimina</string> + <string name="action_send">PUBLICA</string> + <string name="action_send_public">PUBLICA!</string> + <string name="action_retry">Torna a intentar-ho</string> + <string name="action_close">Tanca</string> + <string name="action_view_profile">Perfil</string> + <string name="action_view_preferences">Preferències</string> + <string name="action_view_favourites">Preferits</string> + <string name="action_view_mutes">Usuaris silenciats</string> + <string name="action_view_blocks">Usuaris blocats</string> + <string name="action_view_follow_requests">Peticions de seguiment</string> + <string name="action_view_media">Multimèdia</string> + <string name="action_open_in_web">Obre al navegador</string> + <string name="action_add_media">Afegeix multimèdia</string> + <string name="action_photo_take">Fes una foto</string> + <string name="action_share">Comparteix</string> + <string name="action_mute">Silencia</string> + <string name="action_unmute">Deixa de silenciar</string> + <string name="action_mention">Menciona</string> + <string name="action_hide_media">Amaga el multimèdia</string> + <string name="action_save">Desa</string> + <string name="action_edit_profile">Edita el perfil</string> + <string name="action_undo">Desfés</string> + <string name="action_accept">D\'acord</string> + <string name="action_reject">Rebutja</string> + <string name="action_search">Cerca</string> + <string name="action_access_drafts">Esborranys</string> + <string name="download_image">S\'està baixant %1$s</string> + <string name="action_copy_link">Copia l\'enllaç</string> + <string name="send_post_link_to">Comparteix l\'URL de la publicació a…</string> + <string name="send_post_content_to">Comparteix la publicació a…</string> + <string name="confirmation_reported">Enviat!</string> + <string name="confirmation_unblocked">Usuari desblocat</string> + <string name="confirmation_unmuted">Usuari sense silenciar</string> + <string name="hint_domain">Quina instància?</string> + <string name="hint_compose">Què està passant?</string> + <string name="hint_content_warning">Avís de contingut</string> + <string name="hint_display_name">Nom visible</string> + <string name="hint_note">Biografia</string> + <string name="hint_search">Cerca…</string> + <string name="search_no_results">No hi ha cap resultat</string> + <string name="label_avatar">Avatar</string> + <string name="label_header">Capçalera</string> + <string name="link_whats_an_instance">Què és una instància?</string> + <string name="login_connection">S\'està connectant…</string> + <string name="dialog_whats_an_instance">Aquí pots introduir l\'adreça o domini de qualsevol instància, + com ara mastodont.cat, mastodon.social, icosahedron.website o + <a href="https://instances.social">molts més!</a> + \n\nSi encara no tens cap copte, pots introduir el nom de la instància on t\'agradaria + unir-te i crear un compte allà.\n\nUna instànica és un únic lloc on el teu compte s\'hostatja + , però pots comunicar-te fàcilment i seguir amics d\'altres instàncies com si fossiu en el mateix lloc. + \n\nTens més informació a <a href="https://joinmastodon.org">joinmastodon.org</a>. + </string> + <string name="dialog_title_finishing_media_upload">S\'està finalitzant la pujada de material multimèdia</string> + <string name="dialog_message_uploading_media">S\'està pujant…</string> + <string name="dialog_download_image">Baixa</string> + <string name="dialog_unfollow_warning">Vols deixar de seguir aquest compte?</string> + <string name="visibility_public">Pública: és visible a la cronologia pública</string> + <string name="visibility_unlisted">Sense llistar: no és visible a les cronologies públiques</string> + <string name="visibility_private">Només seguidors: només és visible per al teus seguidors</string> + <string name="visibility_direct">Directa: només és visible per als usuaris mencionats</string> + <string name="pref_title_edit_notification_settings">Notificacions</string> + <string name="pref_title_notifications_enabled">Notificacions</string> + <string name="pref_title_notification_alerts">Alertes</string> + <string name="pref_title_notification_alert_sound">Notifica amb un so</string> + <string name="pref_title_notification_alert_vibrate">Notifica amb una vibració</string> + <string name="pref_title_notification_alert_light">Notifica amb el llum led</string> + <string name="pref_title_notification_filters">Notifica\'m si</string> + <string name="pref_title_notification_filter_mentions">em mencionen</string> + <string name="pref_title_notification_filter_follows">em segueixen</string> + <string name="pref_title_notification_filter_reblogs">retootejen les meves publicacions</string> + <string name="pref_title_notification_filter_favourites">marquen com a preferit les meves publicacions</string> + <string name="pref_title_appearance_settings">Aparença</string> + <string name="pref_title_browser_settings">Navegador</string> + <string name="pref_title_custom_tabs">Pestanyes personalitzades del Chrome</string> + <string name="pref_title_post_filter">Filtre de la cronologia</string> + <string name="pref_title_post_tabs">Pestanyes</string> + <string name="pref_title_show_boosts">Mostra els impulsos</string> + <string name="pref_title_show_replies">Mostra les respostes</string> + <string name="pref_title_show_media_preview">Mostra les previsualitzacions</string> + <string name="pref_default_post_privacy">Privacitat per defecte de les publicacions</string> + <string name="pref_publishing">Publicació</string> + <string name="post_privacy_public">Pública</string> + <string name="post_privacy_unlisted">Sense llistar</string> + <string name="post_privacy_followers_only">Només seguidors</string> + <string name="pref_post_text_size">Mida de text de l\'estat</string> + <string name="notification_mention_name">Noves mencions</string> + <string name="notification_mention_descriptions">Notificacions sobre mencions noves</string> + <string name="notification_follow_name">Nous seguidors</string> + <string name="notification_follow_description">Notificacions sobre nous seguidors</string> + <string name="notification_boost_name">Impulsos</string> + <string name="notification_boost_description">Notificacions quan s\'impulsen les teves publicacions</string> + <string name="notification_favourite_name">Preferits</string> + <string name="notification_favourite_description">Notificacions quan les teves publicacions es marquen com a preferides</string> + <string name="notification_mention_format">%1$s et mencionen</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s i %4$d més</string> + <string name="notification_summary_medium">%1$s, %2$s i %3$s</string> + <string name="notification_summary_small">%1$s i %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d interacció nova</item> + <item quantity="other">%1$d interaccions noves</item> + </plurals> + <string name="description_account_locked">Compte blocat</string> + <string name="about_title_activity">Quant a</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tusky és programari gratuït, lliure i de codi obert. + Està llicenciat en els termes de la Llicència Pública General GNU versió 3. + Podeu trobar les llicència aquí: https://www.gnu.org/licenses/gpl-3.0.ca.html</string> + <!-- note to translators: the url can be changed to link to the localized version of the license --> + <string name="about_project_site"> + Lloc web del projecte:\n + https://tusky.app + </string> + <string name="about_bug_feature_request_site"> + Informes d\'errors i peticions de funcionalitats:\n + https://github.com/tuskyapp/Tusky/issues + </string> + <string name="about_tusky_account">Perfil del Tusky</string> + <string name="post_share_content">Comparteix el contingut de la publicació</string> + <string name="post_share_link">Comparteix l\'enllaç a la publicació</string> + <string name="post_media_images">Imatges</string> + <string name="post_media_video">Vídeo</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">en %1$d anys</string> + <string name="abbreviated_in_days">en %1$dd</string> + <string name="abbreviated_in_hours">en %1$dh</string> + <string name="abbreviated_in_minutes">en %1$dm</string> + <string name="abbreviated_in_seconds">en %1$ds</string> + <string name="abbreviated_years_ago">%1$d anys</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="follows_you">Et segueix</string> + <string name="pref_title_alway_show_sensitive_media">Mostra el contingut no apte (NSFW)</string> + <string name="title_media">Multimèdia</string> + <string name="replying_to">En resposta a @%1$s</string> + <string name="load_more_placeholder_text">carrega\'n més</string> + <string name="poll_vote">Vota</string> + <string name="error_sender_account_gone">S\'ha produït un error en publicar.</string> + <string name="title_tab_preferences">Pestanyes</string> + <string name="title_licenses">Llicències</string> + <string name="post_content_show_more">Amplia</string> + <string name="action_quick_reply">Resposta ràpida</string> + <string name="action_unfavourite">Elimineu els preferits</string> + <string name="action_view_account_preferences">Preferències del compte</string> + <string name="action_edit_own_profile">Edita</string> + <string name="title_direct_messages">Missatges directes</string> + <string name="message_empty">No hi ha res aquí.</string> + <string name="action_unreblog">Elimina l\'impuls</string> + <string name="error_network">S\'ha produït un error de connexió! Comproveu la connexió i torneu-ho a provar!</string> + <string name="post_media_hidden_title">Multimèdia amagada</string> + <string name="post_content_show_less">Amaga</string> + <string name="action_logout_confirm">Estàs segur de tancar la sessió de %1$s\?</string> + <string name="action_hide_reblogs">Amaga els impulsos</string> + <string name="action_show_reblogs">Mostra els impulsos</string> + <string name="action_delete_and_redraft">Elimina i reescriu</string> + <string name="action_open_drawer">Obre el menú</string> + <string name="action_toggle_visibility">Visibilitat de la publicació</string> + <string name="action_content_warning">Contingut sensible</string> + <string name="action_add_tab">Afegir una pestanya</string> + <string name="action_links">Enllaços</string> + <string name="action_mentions">Mencions</string> + <string name="action_hashtags">Hashtags</string> + <string name="action_open_faved_by">Mostra els favorits</string> + <string name="title_hashtags_dialog">Hashtags</string> + <string name="title_mentions_dialog">Mencions</string> + <string name="title_links_dialog">Enllaç</string> + <string name="action_share_as">Comparteix com a…</string> + <string name="download_media">Baixa el fitxer</string> + <string name="send_media_to">Compartir la imatge a …</string> + <string name="state_follow_requested">Petició enviada</string> + <string name="title_posts_with_replies">Amb respostes</string> + <string name="action_emoji_keyboard">Teclat d\'emojis</string> + <string name="action_open_media_n">Obrir el media #%1$d</string> + <string name="action_open_as">Obre com a %1$s</string> + <string name="downloading_media">S\'està Descarregant media</string> + <string name="label_quick_reply">Resposta …</string> + <string name="dialog_message_cancel_follow_request">Revocar la petició de seguiment\?</string> + <string name="dialog_delete_post_warning">Vols eliminar aquest toot\?</string> + <string name="dialog_redraft_post_warning">Vols eliminar i reescriure aquesta publicació\?</string> + <string name="pref_title_notification_filter_poll">Finalització de les enquetes</string> + <string name="pref_title_app_theme">Tema</string> + <string name="pref_title_timelines">Cronologia</string> + <string name="pref_title_timeline_filters">Filtres</string> + <string name="app_them_dark">Fosc</string> + <string name="app_theme_light">Clar</string> + <string name="app_theme_black">Negre</string> + <string name="app_theme_auto">Brillantor automàtica</string> + <string name="app_theme_system">Utilitzar el tema del sistema</string> + <string name="pref_title_language">Idioma</string> + <string name="pref_title_proxy_settings">Proxy</string> + <string name="pref_title_http_proxy_settings">HTTP proxy</string> + <string name="pref_title_http_proxy_enable">Activar el proxy HTTP</string> + <string name="pref_title_http_proxy_server">Servidor del proxy HTTP</string> + <string name="pref_title_http_proxy_port">Port del proxy HTTP</string> + <string name="pref_default_media_sensitivity">Marcar sempre els medias com a sensibles</string> + <string name="pref_failed_to_sync">Error al sincronitzar els paràmetres</string> + <string name="post_text_size_smallest">Petit</string> + <string name="post_text_size_small">Petit</string> + <string name="post_text_size_medium">Mig</string> + <string name="post_text_size_large">Gran</string> + <string name="post_text_size_largest">Més grand</string> + <string name="notification_poll_name">Enquestes</string> + <string name="pref_title_thread_filter_keywords">Converses</string> + <string name="filter_addition_title">Afegir un filtre</string> + <string name="filter_edit_title">Modificar un filtre</string> + <string name="filter_dialog_remove_button">Eliminar</string> + <string name="add_account_name">Afegir un compte</string> + <string name="action_open_reblogger">Obre l\'autor de l\'impuls</string> + <string name="action_open_reblogged_by">Mostra els impulsos</string> + <string name="notification_poll_description">Notificacions d\'enquestes que han finalitzat</string> + <string name="pref_title_public_filter_keywords">Línia de temps públiques</string> + <string name="filter_dialog_update_button">Actualització</string> + <string name="filter_add_description">Frase per filtrar</string> + <string name="add_account_description">Afegir un compte de Mastodont</string> + <string name="action_lists">Llistes</string> + <string name="title_lists">Llistes</string> + <string name="error_create_list">És impossible crear la llista</string> + <string name="error_rename_list">Impossible reanomenar la llista</string> + <string name="error_delete_list">És impossible suprimir la llista</string> + <string name="action_create_list">Crear una llista</string> + <string name="action_rename_list">Reanomenar la llista</string> + <string name="action_delete_list">Suprimir la llista</string> + <string name="hint_search_people_list">Cercar persones que segueixes</string> + <string name="action_add_to_list">Afegir un compte a la llista</string> + <string name="action_remove_from_list">Suprimir un compte de la llista</string> + <string name="compose_active_account_description">"Publicar com a %1$s"</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Descriu per a persones amb discapacitat visual +\n(%1$d límit de caràcters)</item> + <item quantity="other">Descriu per a persones amb discapacitat visual +\n(%1$d límit de caràcters)</item> + </plurals> + <string name="action_set_caption">Afegir una llegenda</string> + <string name="action_remove">Eliminar</string> + <string name="lock_account_label">Protegir el compte</string> + <string name="lock_account_label_description">S\'haurà d\'admetre els seguidors manualment</string> + <string name="compose_save_draft">Guardar l\'esborrany\?</string> + <string name="send_post_notification_title">S\'està publicant…</string> + <string name="send_post_notification_error_title">Error en publicar</string> + <string name="send_post_notification_channel_name">S\'esatan enviant les publicacions</string> + <string name="send_post_notification_cancel_title">Envio anul·lat</string> + <string name="send_post_notification_saved_content">S\'ha guardat una còpia de la publicació als esborranys</string> + <string name="action_compose_shortcut">Escriure</string> + <string name="error_no_custom_emojis">La teva instància %1$s no te emojis personalitzats</string> + <string name="emoji_style">Estil dels emojis</string> + <string name="system_default">Sistema per defecte</string> + <string name="download_fonts">Hauràs de descarregar el joc d\'emojis</string> + <string name="performing_lookup_title">Cercant…</string> + <string name="expand_collapse_all_posts">Expandir/ocultar tots els estats</string> + <string name="action_open_post">Obre la publicació</string> + <string name="restart_required">Cal reiniciar l\'aplicació</string> + <string name="restart_emoji">Has de reiniciar l\'aplicació per tal d\'aplicar aquests canvis</string> + <string name="later">Més tard</string> + <string name="restart">Reiniciar</string> + <string name="caption_systememoji">Conjunt d\'emojis predeterminats del teu dispositiu</string> + <string name="caption_blobmoji">Els emojis d\'Android 4.4 a 7.1</string> + <string name="caption_twemoji">Emojis estàndard de Mastodont</string> + <string name="download_failed">Descàrrega fallida</string> + <string name="profile_badge_bot_text">Bot</string> + <string name="account_moved_description">%1$s s\'ha trasllat a:</string> + <string name="reblog_private">Tornar a impulsar</string> + <string name="unreblog_private">No impulsar</string> + <string name="license_description">Tusky conté codi i recursos dels següents projectes:</string> + <string name="license_apache_2">Sota llicencia Apache (text a sota)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Metadades del perfil</string> + <string name="profile_metadata_add">Afegir dada</string> + <string name="profile_metadata_label_label">Etiqueta</string> + <string name="profile_metadata_content_label">Contingut</string> + <string name="pref_title_absolute_time">Utilitzar el temps absolut</string> + <string name="label_remote_account">La informació de sota pot mostrar el perfil incomplert de l\'usuari. Clica per obrir el perfil complert al navegador.</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Favorit</item> + <item quantity="other"><b>%1$s</b> Favorits</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> impuls</item> + <item quantity="other"><b>%1$s</b> impulsos</item> + </plurals> + <string name="title_reblogged_by">Impulsat per</string> + <string name="title_favourited_by">Marcat favorit per</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s i %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s i %3$d més</string> + <string name="description_post_media">Mèdia : %1$s</string> + <string name="description_post_media_no_description_placeholder">Sense descripció</string> + <string name="description_post_favourited">Favorits</string> + <string name="description_visibility_public">Públic</string> + <string name="description_visibility_unlisted">Sense llistar</string> + <string name="description_visibility_private">Seguidors</string> + <string name="description_visibility_direct">Directe</string> + <string name="hint_list_name">Nom de la llista</string> + <string name="edit_hashtag_hint">Hashtag sense #</string> + <string name="notifications_clear">Netejar</string> + <string name="notifications_apply_filter">Filtrar</string> + <string name="filter_apply">Aplicar</string> + <string name="compose_shortcut_long_label">Escriure una publicació</string> + <string name="compose_shortcut_short_label">Escriure</string> + <string name="pref_title_bot_overlay">Mostra l\'indicador dels bots</string> + <string name="notification_clear_text">Vols netejar totes les notificacions permanentment\?</string> + <string name="poll_info_format"> <!-- 15 vots • queda 1 hora --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s vot</item> + <item quantity="other">%1$s vots</item> + </plurals> + <string name="poll_info_time_absolute">Acaba a %1$s</string> + <string name="poll_info_closed">tancat</string> + <string name="poll_ended_voted">L\'enquesta on has votat està tancada</string> + <string name="poll_ended_created">La enquesta que heu creat ha finalitzat</string> + <string name="description_post_cw">Advertència: %1$s</string> + <string name="title_posts_pinned">Fixat</string> + <string name="unpin_action">No fixis</string> + <string name="pin_action">Fixar</string> + <string name="description_post_reblogged">Respost</string> + <string name="compose_preview_image_description">Accions per a la imatge %1$s</string> + <string name="pref_title_animate_gif_avatars">Activar l\'animació dels GIF</string> + <string name="title_domain_mutes">Dominis ocults</string> + <string name="action_view_domain_mutes">Dominis ocults</string> + <string name="action_mute_domain">Silenciar %1$s</string> + <string name="confirmation_domain_unmuted">%1$s visible</string> + <string name="mute_domain_warning_dialog_ok">Amagar el domini sencer</string> + <string name="pref_title_alway_open_spoiler">Mostra sempre obertes les publicacions marcades amb avisos de contingut</string> + <string name="filter_dialog_whole_word">Paraula sencera</string> + <string name="caption_notoemoji">Ventall actual d\'emojis de Google</string> + <string name="description_poll">Enquesta amb opcions: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="button_continue">Continuar</string> + <string name="button_back">Enrere</string> + <string name="button_done">Fet</string> + <string name="hint_additional_info">Comentaris addicionals</string> + <string name="report_remote_instance">Reenviar a %1$s</string> + <string name="failed_report">Report erroni</string> + <string name="failed_fetch_posts">Error obtenint els estats</string> + <string name="report_description_1">L\'informe s\'enviarà al moderador del teu servidor. Pots afegir una explicació del motiu d\'aquest informe del compte a sota:</string> + <string name="title_accounts">Comptes</string> + <string name="mute_domain_warning">Estàs segur que vols bloquejar tot de %1$s\? No veuràs cap contingut de domini ni en els fils públics ni a les teves notificacions. Els teus seguidors d\'aquest domini seran eliminats.</string> + <string name="filter_dialog_whole_word_description">Quan la paraula o la frase siguin només alfanumèrica , només s\'aplicarà si coincideix amb tota la paraula</string> + <string name="report_sent_success">\@%1$s reportat satisfactoriament</string> + <string name="report_description_remote_instance">El compte és d\'un altre servidor. Enviar, igualment, una copia anònima del report\?</string> + <string name="failed_search">Cerca fallida</string> + <string name="duration_1_hour">1 hora</string> + <string name="duration_6_hours">6 hores</string> + <string name="edit_poll">Edita</string> + <string name="action_add_poll">Afegeix una enquesta</string> + <string name="create_poll_title">Enquesta</string> + <string name="duration_5_min">5 minuts</string> + <string name="duration_30_min">30 minuts</string> + <string name="duration_1_day">1 dia</string> + <string name="duration_3_days">3 dies</string> + <string name="duration_7_days">7 dies</string> + <string name="add_poll_choice">Afegeix una tria</string> + <string name="poll_allow_multiple_choices">Múltiples tries</string> + <string name="poll_new_choice_hint">Tria %1$d</string> + <string name="title_bookmarks">Preferits</string> + <string name="title_scheduled_posts">Publicacions programades</string> + <string name="action_bookmark">Preferit</string> + <string name="action_edit">Edita</string> + <string name="action_view_bookmarks">Preferits</string> + <string name="action_access_scheduled_posts">Publicacions programades</string> + <string name="action_schedule_post">Programa la publicació</string> + <string name="action_reset_schedule">Reiniciar</string> + <string name="about_powered_by_tusky">Desenvolupat per Tusky</string> + <string name="description_post_bookmarked">S\'ha afegit a les adreces d\'interès</string> + <string name="select_list_title">Seleccionar la llista</string> + <string name="list">Llista</string> + <string name="post_lookup_error_format">S\'ha produït un error en cercar la publicació %1$s</string> + <string name="no_scheduled_posts">No tens cap estat planificat.</string> + <string name="no_drafts">No teniu cap esborrany.</string> + <string name="warning_scheduling_interval">L\'interval mínim de planificació a Mastodon és de 5 minuts.</string> + <string name="notification_follow_request_name">Peticions de seguiment</string> + <string name="pref_title_confirm_reblogs">Mostra el diàleg de confirmació abans de promoure</string> + <string name="pref_title_show_cards_in_timelines">Mostra les previsualitzacions dels enllaços en els fils</string> + <string name="pref_title_enable_swipe_for_tabs">Habilita el gest de desplaçament per despleçar-te entre pestanyes</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s persona</item> + <item quantity="other">%1$s persones</item> + </plurals> + <string name="hashtags">Hashtags</string> + <string name="add_hashtag_title">Afegir hashtag</string> + <string name="notification_follow_request_description">Notificacions sobre sol·licituds de seguiment</string> + <string name="pref_title_notification_filter_follow_requests">sol·licitació de seguiment</string> + <string name="dialog_mute_warning">Silenciar @%1$s\?</string> + <string name="dialog_block_warning">Bloquejar @%1$s\?</string> + <string name="action_unmute_conversation">No silenciar la conversació</string> + <string name="action_mute_conversation">Silencia la conversa</string> + <string name="notification_follow_request_format">%1$s ha sol·licitat seguir-te</string> + <string name="pref_main_nav_position_option_bottom">A baix</string> + <string name="pref_main_nav_position_option_top">A dalt</string> + <string name="pref_main_nav_position">Posició de navegació principal</string> + <string name="pref_title_gradient_for_media">Mostra els degradats de colors per als mitjans ocults</string> + <string name="dialog_mute_hide_notifications">Amagar notificacions</string> + <string name="action_unmute_domain">Deixar de silenciar %1$s</string> + <string name="action_unmute_desc">Deixar de silenciar %1$s</string> + <string name="review_notifications">Revisió d\'avisos</string> + <string name="account_note_saved">S\'ha desat!</string> + <string name="account_note_hint">Les vostres notes quant a aquest compte</string> + <string name="pref_title_wellbeing_mode">Benestar</string> + <string name="pref_title_hide_top_toolbar">Amaga el títol de la barra d\'eines superior</string> + <string name="no_announcements">No hi ha cap avís.</string> + <string name="duration_indefinite">Indefinit</string> + <string name="label_duration">Durada</string> + <plurals name="poll_timespan_seconds"> + <item quantity="one">falta %1$d segon</item> + <item quantity="other">falten %1$d segons</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">falta %1$d minut</item> + <item quantity="other">falten %1$d minuts</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">falta %1$d hora</item> + <item quantity="other">falten %1$d hores</item> + </plurals> + <plurals name="poll_timespan_days"> + <item quantity="one">falta %1$d dia</item> + <item quantity="other">falten %1$d dies</item> + </plurals> + <string name="post_media_attachments">Adjuncions</string> + <string name="post_media_audio">Àudio</string> + <string name="notification_subscription_description">Notificacions quan algú a qui estàs subscrit publica una publicació nova</string> + <string name="notification_subscription_name">Publicacions noves</string> + <string name="pref_title_animate_custom_emojis">emojis personalitzats animats</string> + <string name="pref_title_notification_filter_subscriptions">algú a qui estic subscrit ha publicat una nova publicació</string> + <string name="notification_subscription_format">%1$s acaba de fer una publicació</string> + <string name="title_announcements">Avisos</string> + <string name="drafts_post_reply_removed">S\'ha eliminat la publicació a la qual vau fer un esborrany de resposta</string> + <string name="draft_deleted">S\'ha eliminat l\'esborrany</string> + <string name="drafts_failed_loading_reply">No s\'ha pogut carregar la informació de la resposta</string> + <string name="drafts_post_failed_to_send">No s\'ha pogut publicar!</string> + <string name="dialog_delete_list_warning">Segur que voleu esborrar la llista %1$s\?</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">No podeu penjar més de %1$d fitxers adjunts multimèdia.</item> + <item quantity="other">No podeu penjar més de %1$d fitxers adjunts multimèdia.</item> + </plurals> + <string name="wellbeing_hide_stats_profile">Amaga les estadístiques quantitatives dels perfils</string> + <string name="wellbeing_hide_stats_posts">Amaga les estadístiques quantitatives de les publicacions</string> + <string name="limit_notifications">Limita les notificacions de la cronologia</string> + <string name="error_could_not_load_login_page">No s\'ha pogut carregar la pàgina d\'inici de sessió.</string> + <string name="error_loading_account_details">No s\'han pogut carregar els detalls del compte</string> + <string name="action_add_or_remove_from_list">Afegeix o elimina de la llista</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="dialog_delete_conversation_warning">Vols suprimir aquesta conversa\?</string> + <string name="failed_to_add_to_list">No s\'ha pogut afegir el compte a la llista</string> + <string name="error_multimedia_size_limit">Els fitxers de vídeo i àudio no poden superar la mida de %1$s MB.</string> + <string name="error_image_edit_failed">La imatge no s\'ha pogut editar.</string> + <string name="action_dismiss">Descartar</string> + <string name="pref_title_http_proxy_port_message">El port hauria d\'estar entre %1$d i %2$d</string> + <string name="status_count_one_plus">+1</string> + <string name="title_edits">Edicions</string> + <string name="pref_title_notification_filter_reports">hi ha un nou informe</string> + <string name="pref_summary_http_proxy_disabled">Inhabilitat</string> + <string name="pref_summary_http_proxy_missing"><no definit></string> + <string name="pref_summary_http_proxy_invalid"><no vàlid></string> + <string name="error_following_hashtags_unsupported">Aquesta instància no admet seguir hashtags.</string> + <string name="notification_sign_up_format">%1$s s\'ha registrat</string> + <string name="pref_show_self_username_always">Sempre</string> + <string name="pref_show_self_username_disambiguate">Quan s\'inicien la sessió amb diversos comptes</string> + <string name="pref_show_self_username_never">Mai</string> + <string name="error_unmuting_hashtag_format">S\'ha produït un error en activar #%1$s</string> + <string name="error_muting_hashtag_format">S\'ha produït un error en silenciar #%1$s</string> + <string name="post_edited">S\'ha editat %1$s</string> + <string name="notification_update_name">Edicions de publicacions</string> + <string name="action_post_failed">La càrrega ha fallat</string> + <string name="action_post_failed_detail">La teva publicació no s\'ha pogut penjar i s\'ha desat als esborranys. +\n +\nNo s\'ha pogut contactar amb el servidor o bé ha rebutjat la publicació.</string> + <string name="action_post_failed_detail_plural">Les teves publicacions no s\'han pogut penjar i s\'han desat als esborranys. +\n +\nNo s\'ha pogut contactar amb el servidor o ha rebutjat les publicacions.</string> + <string name="action_post_failed_show_drafts">Mostra esborranys</string> + <string name="action_post_failed_do_nothing">Descartar</string> + <string name="send_account_link_to">Comparteix l\'URL del compte a…</string> + <string name="post_media_alt">ALT</string> + <string name="notification_update_format">%1$s ha editat la seva publicació</string> + <string name="notification_report_format">Nou informe sobre %1$s</string> + <string name="notification_header_report_format">%1$s ha informat %2$s</string> + <string name="notification_summary_report_format">%1$s · %2$d publicacions adjuntes</string> + <string name="action_unbookmark">Elimina el marcador</string> + <string name="action_delete_conversation">Suprimeix la conversa</string> + <string name="notification_sign_up_name">Inscripcions</string> + <string name="action_continue_edit">Continua editant</string> + <string name="action_discard">Descartar els canvis</string> + <string name="hint_media_description_missing">L\'arxiu multimèdia han de tenir una descripció.</string> + <string name="pref_default_post_language">Idioma de publicació per defecte</string> + <string name="notification_update_description">Les notificacions quan s\'editen les publicacions amb les quals has interaccionat</string> + <string name="notification_report_description">Notificacions sobre informes de moderació</string> + <string name="status_created_at_now">ara</string> + <string name="action_browser_login">Inicieu sessió amb el navegador</string> + <string name="action_add_reaction">afegir reacció</string> + <string name="action_share_account_link">Comparteix l\'enllaç al compte</string> + <string name="action_share_account_username">Comparteix el nom d\'usuari del compte</string> + <string name="action_details">Detalls</string> + <string name="send_account_username_to">Comparteix el nom d\'usuari del compte a…</string> + <string name="account_username_copied">S\'ha copiat el nom d\'usuari</string> + <string name="confirmation_hashtag_unfollowed">#%1$s deixat de seguir</string> + <string name="pref_title_notification_filter_sign_ups">algú s\'ha donat d\'alta</string> + <string name="pref_title_notification_filter_updates">una publicació amb la qual he interactuat s\'ha editat</string> + <string name="notification_report_name">Informes</string> + <string name="error_following_hashtag_format">Error seguint #%1$s</string> + <string name="error_unfollowing_hashtag_format">Error en deixar de seguir #%1$s</string> + <string name="error_status_source_load">No s\'ha pogut carregar la font d\'estat des del servidor.</string> + <string name="title_followed_hashtags">Hashtags seguits</string> + <string name="notification_sign_up_description">Notificacions sobre nous usuaris</string> + <string name="title_login">Iniciar Sessió</string> + <string name="title_migration_relogin">Torneu a iniciar sessió per rebre notificacions push</string> + <string name="a11y_label_loading_thread">S\'està carregant el fil</string> + <string name="compose_save_draft_loses_media">Guardar l\'esborrany\? (Els fitxers adjunts es tornaran a penjar quan recupereu l\'esborrany.)</string> + <string name="dialog_push_notification_migration_other_accounts">Heu tornat a iniciar sessió al vostre compte actual per concedir permís de subscripció push a Tusky. Tanmateix, encara teniu altres comptes que no s\'han migrat d\'aquesta manera. Canvieu-los i torneu a iniciar sessió un per un per activar el suport de notificacions UnifiedPush.</string> + <string name="instance_rule_info">En iniciar sessió, accepteu les regles de %1$s.</string> + <string name="action_edit_image">Edita la imatge</string> + <string name="failed_to_pin">No s\'ha pogut fixar</string> + <string name="failed_to_unpin">No s\'ha pogut desfixar</string> + <string name="description_post_language">Idioma de la publicació</string> + <string name="duration_14_days">14 dies</string> + <string name="follow_requests_info">Tot i que el vostre compte no està bloquejat, el personal de %1$s va pensar que potser voldreu revisar les sol·licituds de seguiment d\'aquests comptes manualment.</string> + <string name="report_category_other">Altres</string> + <string name="pref_title_reading_order">Ordre de lectura</string> + <string name="pref_reading_order_oldest_first">El més vell primer</string> + <string name="pref_reading_order_newest_first">El més nou primer</string> + <string name="duration_30_days">30 dies</string> + <string name="failed_to_remove_from_list">No s\'ha pogut eliminar el compte de la llista</string> + <string name="duration_60_days">60 dies</string> + <string name="duration_90_days">90 dies</string> + <string name="duration_no_change">(Sense canvis)</string> + <string name="tusky_compose_post_quicksetting_label">Redacta la publicació</string> + <string name="pref_title_confirm_favourites">Mostra el diàleg de confirmació abans de marcar com a preferit</string> + <string name="action_unfollow_hashtag_format">Deixar de seguir #%1$s\?</string> + <string name="compose_unsaved_changes">Tens canvis no desats.</string> + <string name="set_focus_description">Toqueu o arrossegueu el cercle per triar el punt focal que sempre serà visible a les miniatures.</string> + <string name="pref_title_show_self_username">Mostra el nom d\'usuari a les barres d\'eines</string> + <string name="action_set_focus">Estableix el punt d\'enfocament</string> + <string name="description_login">Funciona en la majoria dels casos. No es filtra cap dada a altres aplicacions.</string> + <string name="description_browser_login">Pot ser compatible amb mètodes d\'autenticació addicionals, però requereix un navegador compatible.</string> + <string name="no_lists">No tens cap llista.</string> + <string name="delete_scheduled_post_warning">Vols suprimir aquesta publicació programada\?</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="report_category_violation">Incompliment de la regla</string> + <string name="report_category_spam">Spam</string> + <string name="duration_180_days">180 dies</string> + <string name="duration_365_days">365 dies</string> + <string name="wellbeing_mode_notice">S\'amagarà algunes dades que poden afectar el vostre benestar mental. Això inclou: +\n +\n - Notificacions de favorits/impulsos/seguiments +\n - Preferits/augmenta el nombre de publicacions +\n - Estadístiques de seguidors/publicació als perfils +\n +\n Les notificacions push no es veuran afectades, però podeu revisar les vostres preferències de notificació manualment.</string> + <string name="account_date_joined">S\'ha unit el %1$s</string> + <string name="saving_draft">Desant l\'esborrany…</string> + <string name="dialog_push_notification_migration">Per utilitzar les notificacions push mitjançant UnifiedPush, Tusky necessita permís per subscriure\'s a les notificacions al vostre servidor Mastodon. Això requereix un nou inici de sessió per canviar els àmbits d\'OAuth concedits a Tusky. Si feu servir l\'opció de tornar a iniciar sessió aquí o a les preferències del compte, es conservaran tots els esborranys locals i la memòria cau.</string> + <string name="tips_push_notification_migration">Torneu a iniciar sessió a tots els comptes per activar el suport de notificacions push.</string> + <string name="description_post_edited">Editat</string> + <string name="status_edit_info">%1$s ha editat</string> + <string name="action_subscribe_account">Subscriu-te</string> + <string name="action_unsubscribe_account">Cancel·la la subscripció</string> + <string name="instance_rule_title">%1$s regles</string> + <string name="mute_notifications_switch">Silencia les notificacions</string> + <string name="status_created_info">%1$s ha creat</string> + <string name="title_public_trending_hashtags">Hashtags populars</string> + <string name="accessibility_talking_about_tag">%1$d persones parlen del hashtag %2$s</string> + <string name="total_usage">Ús total</string> + <string name="total_accounts">Total de comptes</string> + <string name="dialog_follow_hashtag_title">Segueix hashtag</string> + <string name="dialog_follow_hashtag_hint">#hashtag</string> + <string name="action_refresh">Actualitzar</string> + <string name="notification_unknown_name">Desconegut</string> + <string name="status_filter_placeholder_label_format">Filtrat: %1$s</string> + <string name="pref_title_account_filter_keywords">Perfils</string> + <string name="status_filtered_show_anyway">Mostra de totes maneres</string> + <string name="socket_timeout_exception">El contacte amb el teu servidor ha trigat massa</string> + <string name="ui_error_unknown">motiu desconegut</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml new file mode 100644 index 0000000..a592aa0 --- /dev/null +++ b/app/src/main/res/values-ckb/strings.xml @@ -0,0 +1,471 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="hint_compose">چی خەریکه ڕوودەدات؟</string> + <string name="hint_domain">کام نموونە؟</string> + <string name="confirmation_domain_unmuted">%1$s نەشاراوە</string> + <string name="confirmation_unmuted">بەکارهێنەر نەگۆڕاو</string> + <string name="confirmation_unblocked">بەکارهێنەر بەربەست نەکراوە</string> + <string name="confirmation_reported">ناردن!</string> + <string name="send_media_to">هاوبەشکردنی میدیا بۆ…</string> + <string name="send_post_content_to">هاوبەشی کردن بە توت بۆ…</string> + <string name="send_post_link_to">هاوبەشکردنی توتی URL بۆ…</string> + <string name="downloading_media">داگرتنی میدیا</string> + <string name="download_media">داگرتنی میدیا</string> + <string name="action_share_as">هاوبەش کردن وەک …</string> + <string name="action_open_as">کردنەوە وەک %1$s</string> + <string name="action_copy_link">بەستەرەکە ڕوونوس بکە</string> + <string name="download_image">داگرتنی %1$s</string> + <string name="action_open_media_n">کردنەوەی میدیا #%1$d</string> + <string name="title_links_dialog">بەستەرەکان</string> + <string name="title_mentions_dialog">ئاماژەکان</string> + <string name="title_hashtags_dialog">هاشتاگی</string> + <string name="action_open_faved_by">پیشاندانی دڵخوازەکان</string> + <string name="action_open_reblogged_by">پیشاندانی بەهێزکردنەکان</string> + <string name="action_open_reblogger">پۆستکەرەوەکە ببینە</string> + <string name="action_hashtags">هاشتاگ</string> + <string name="action_mentions">ئاماژەکان</string> + <string name="action_links">بەستەرەکان</string> + <string name="action_add_tab">زیادکردنی سەرخشت</string> + <string name="action_reset_schedule">ڕیسێت کردن</string> + <string name="action_schedule_post">خشتەی توت</string> + <string name="action_emoji_keyboard">تەختەکلیلی ئیمۆجی</string> + <string name="action_content_warning">ئاگاداری ناوەڕۆک</string> + <string name="action_toggle_visibility">بینینی توت</string> + <string name="action_access_scheduled_posts">توتی خشتەکراو</string> + <string name="action_access_drafts">ڕەشنووسەکان</string> + <string name="action_search">گەڕان</string> + <string name="action_reject">ڕەتکردنەوە</string> + <string name="action_accept">ڕازیبون</string> + <string name="action_undo">گەڕانەوە</string> + <string name="action_edit_own_profile">بژارکردن</string> + <string name="action_edit_profile">دەستکاری پرۆفایل بکە</string> + <string name="action_save">بپارێزە</string> + <string name="action_open_drawer">کردنەوەی وێنەکێش</string> + <string name="action_hide_media">شاردنەوەی میدیا</string> + <string name="action_mention">ئاماژە</string> + <string name="action_unmute_conversation">گفتوگۆی لاببە</string> + <string name="action_unmute_domain">نابێدەنگ کردن %1$s</string> + <string name="action_mute_domain">بێدەنگکردن %1$s</string> + <string name="action_unmute_desc">نابێدەنگ %1$s</string> + <string name="action_unmute">بێدەنگی لابردن</string> + <string name="action_mute">بێدەنگ</string> + <string name="action_share">هاوبەش کردن</string> + <string name="action_photo_take">وێنە بگرە</string> + <string name="action_add_poll">زیادکردنی ڕاپرسی</string> + <string name="action_add_media">زیادکردنی میدیا</string> + <string name="action_open_in_web">لە وێبگەڕ بیکەوە</string> + <string name="action_view_media">میدیا</string> + <string name="action_view_follow_requests">بەدواداچونی داواکاریەکان بکە</string> + <string name="action_view_domain_mutes">دۆمەینە شاراوەکان</string> + <string name="action_view_blocks">بەکارهێنەرە بلۆککراوەکان</string> + <string name="action_view_mutes">بەکارهێنەرە گۆڕاوەکان</string> + <string name="action_view_bookmarks">نیشانەکان</string> + <string name="action_view_favourites">بەدڵبوونەکان</string> + <string name="action_view_account_preferences">پەسەندکراوەکانی ئەژمێر</string> + <string name="action_view_preferences">پەسەندەکان</string> + <string name="action_view_profile">پرۆفایل</string> + <string name="action_close">دابخە</string> + <string name="action_retry">دووبارە هەوڵ بدە</string> + <string name="action_send_public">توت!</string> + <string name="action_send">توت</string> + <string name="action_delete_and_redraft">سڕینەوە و دووبارە-ڕەشنووس</string> + <string name="action_delete">سڕینەوە</string> + <string name="action_edit">دەستکاری</string> + <string name="action_report">گوزارشەکان</string> + <string name="action_show_reblogs">پۆستکردنەوەکان نیشان بدە</string> + <string name="action_hide_reblogs">شاردنەوەی بەهێزکردنەکان</string> + <string name="action_unblock">بەربەست کردن لاببە</string> + <string name="action_block">بلۆک</string> + <string name="action_unfollow">بەدوادانەچو</string> + <string name="action_follow">شوێنی بکەوە</string> + <string name="action_logout_confirm">ئایا دڵنیایت لەوەی دەتەوێت بچیتەدەرەوە لە هەژماری %1$s؟</string> + <string name="action_logout">چوونەدەرەوە</string> + <string name="action_login">چوونەژوورەوە لەگەڵ ماستۆدۆن</string> + <string name="action_compose">دروستکردن</string> + <string name="action_more">زیاتر</string> + <string name="action_unfavourite">لابردنی دڵخوازەکان</string> + <string name="action_bookmark">نیشانه</string> + <string name="action_favourite">دڵخواز</string> + <string name="action_unreblog">پۆستکردنەوەکە بگەڕێنەوە</string> + <string name="action_reblog">بەهێزکردن</string> + <string name="action_reply">وەڵام</string> + <string name="action_quick_reply">وەڵامدانەوەی خێرا</string> + <string name="report_comment_hint">سەرنجەکانی تر؟</string> + <string name="report_username_format">گوزارشت @%1$s</string> + <string name="notification_subscription_format">%1$s تەنها بڵاوکرایەوە</string> + <string name="notification_follow_request_format">%1$s داواکراوە کە شوێنت بکەوێت</string> + <string name="notification_follow_format">%1$s بەدواتا کەوت</string> + <string name="notification_favourite_format">%1$s خۆشترین توتەکەت</string> + <string name="notification_reblog_format">%1$s توتەکەتی بەرزکردەوە</string> + <string name="footer_empty">هیچ شتێک لێرە نیە ڕاکە خوارەوە بۆ نوێکردنەوە!</string> + <string name="message_empty">هیچ شتێک لێرە نیە.</string> + <string name="post_content_show_less">نوشتانەوە</string> + <string name="post_content_show_more">فراوانکردن</string> + <string name="post_content_warning_show_less">کەمتر نیشان بدە</string> + <string name="post_content_warning_show_more">زیاتر پیشان بدە</string> + <string name="post_sensitive_media_directions">کرتە بکە بۆ بینین</string> + <string name="post_media_hidden_title">میدیا شاراوە</string> + <string name="post_sensitive_media_title">ناوەڕۆکی هەستیار</string> + <string name="post_boosted_format">%1$s پۆستی کردەوە</string> + <string name="post_username_format">\@%1$s</string> + <string name="title_licenses">مۆڵەتەکان</string> + <string name="title_announcements">ڕاگه یه نراوەکان</string> + <string name="title_scheduled_posts">توتی خشتەکراو</string> + <string name="title_edit_profile">دەستکاری پرۆفایلەکەت بکە</string> + <string name="title_follow_requests">بەدواداچونی داواکاریەکان بکە</string> + <string name="title_domain_mutes">دۆمەینە شاراوەکان</string> + <string name="title_blocks">بەکارهێنەرە بلۆککراوەکان</string> + <string name="title_mutes">بەکارهێنەرە بێدەنگ</string> + <string name="title_bookmarks">نیشانەکان</string> + <string name="title_favourites">دڵخوازەکان</string> + <string name="title_followers">شوێنکەوتوو</string> + <string name="title_follows">شوێنکەوتنەکان</string> + <string name="title_posts_pinned">چەسپا</string> + <string name="title_posts_with_replies">لەگەڵ وەڵامەکان</string> + <string name="title_posts">پۆست</string> + <string name="title_view_thread">زنجیرە</string> + <string name="title_tab_preferences">سەرخشتەکان</string> + <string name="title_direct_messages">نامە ڕاستەوخۆکان</string> + <string name="title_public_federated">گشتی</string> + <string name="title_public_local">ناوخۆیی</string> + <string name="title_notifications">ئاگادارییەکان</string> + <string name="title_home">سەرەتا</string> + <string name="error_sender_account_gone">هەڵە لە ناردنی توت.</string> + <string name="error_media_upload_sending">بارکردن سەرکەوتوو نەبوو.</string> + <string name="error_media_upload_image_or_video">وێنە و ڤیدیۆکان ناتوانرێت هەردووک هاوپێچ بکرێت لەگەڵ یەک دۆخ.</string> + <string name="error_media_download_permission">مۆڵەت بۆ پاشکەوتکردنی میدیا پێویستە.</string> + <string name="error_media_upload_permission">مۆڵەت بۆ خوێندنەوەی میدیا پێویستە.</string> + <string name="error_media_upload_opening">ئەم فایلە ناتوانرێت بکرێتەوە.</string> + <string name="error_media_upload_type">ناتوانیت لەم جۆرە فایلانە بەرز بکەیتەوە.</string> + <string name="error_compose_character_limit">ئەم نووسینە زۆر درێژە!</string> + <string name="error_retrieving_oauth_token">سەرکەوتوو نەبوو لە بەدەستهێنانی نیشانەی چوونەژوورەوە.</string> + <string name="error_authorization_denied">ڕێپێدان ڕەتکرایەوە.</string> + <string name="error_authorization_unknown">هەڵەیەک بۆ مۆڵەتدانی نەناسراو ڕووی دا.</string> + <string name="error_no_web_browser_found">نەیتوانی وێبگەڕبدۆزێتەوە بۆ بەکارهێنان.</string> + <string name="error_failed_app_registration">سەرکەوتوو نەبوو، ڕاستکردنەوە لەگەڵ ئەم نمونەیە.</string> + <string name="error_invalid_domain">دۆمەینێکی نادروستت نووسیوە</string> + <string name="error_empty">ناکرێت ئەمە بەتاڵ بێت.</string> + <string name="error_network">هەڵەیەک لە پەیوەندییەکەدا ڕوویدا. تکایە دڵنیا ببەوە لە بەردەستبوونی هێڵی ئینتەرنێت.</string> + <string name="error_generic">هەڵەیەک ڕوویدا.</string> + <string name="pref_default_post_privacy">تایبەتمەندی بابەت گریمانەیی</string> + <string name="pref_title_http_proxy_port">دەرگای پرۆکسی HTTP</string> + <string name="pref_title_http_proxy_server">ڕاژەکاری پرۆکسی HTTP</string> + <string name="pref_title_http_proxy_enable">چالاککردنی پرۆکسی HTTP</string> + <string name="pref_title_http_proxy_settings">HTTP proxy</string> + <string name="pref_title_proxy_settings">پرۆکسی</string> + <string name="pref_title_show_media_preview">داگرتنی پێشبینینی میدیا</string> + <string name="pref_title_show_replies">وەڵامدانەوەکان پیشان بدە</string> + <string name="pref_title_show_boosts">پیشاندانی بەهێزکردنەکان</string> + <string name="pref_title_post_tabs">سەرخشتەکان</string> + <string name="pref_title_post_filter">فلتەرکردنی تایملاین</string> + <string name="pref_title_gradient_for_media">نمرەی لاری ڕەنگاوڕەنگ نیشان بدە بۆ میدیای شاراوە</string> + <string name="pref_title_animate_gif_avatars">وێنۆجکەی ئەنیمەی GIF</string> + <string name="pref_title_bot_overlay">نیشاندەر نیشاندەر بۆ بۆتەکان نیشان بدە</string> + <string name="pref_title_language">زمان</string> + <string name="pref_title_custom_tabs">بەکارهێنانی خشتەبەندەکانی دڵخواز</string> + <string name="pref_title_browser_settings">وێبگەڕ</string> + <string name="app_theme_system">دیزاینی سیستەم بەکاربهێنە</string> + <string name="app_theme_auto">خۆکار لە کاتی خۆرئاوابووندا</string> + <string name="app_theme_black">ڕەش</string> + <string name="app_theme_light">ڕووناکی</string> + <string name="app_them_dark">تاریک</string> + <string name="pref_title_timeline_filters">فلتەرەکان</string> + <string name="pref_title_app_theme">ڕووکاری ئەپ</string> + <string name="pref_title_timelines">تایملاین</string> + <string name="pref_title_appearance_settings">دەرکەوتن</string> + <string name="pref_title_notification_filter_subscriptions">کەسێک کە من بەشدارم لە بڵاو کردنەوەی توتێکی نوێیرکری</string> + <string name="pref_title_notification_filter_poll">ڕاپرسی کۆتایی هاتووە</string> + <string name="pref_title_notification_filter_favourites">بابەتەکانی من پەسەندن</string> + <string name="pref_title_notification_filter_reblogs">پۆستەکانم بەرزدەکرانەوه</string> + <string name="pref_title_notification_filter_follow_requests">بەدواداچوونەوەی داواکراو</string> + <string name="pref_title_notification_filter_follows">بەدوادا</string> + <string name="pref_title_notification_filter_mentions">ناوبراو</string> + <string name="pref_title_notification_filters">ئاگادارم بکەوە کاتێک</string> + <string name="pref_title_notification_alert_light">ئاگاداربکەوە بە ڕووناکی</string> + <string name="pref_title_notification_alert_vibrate">ئاگادارکردنەوەی لەلەرینە</string> + <string name="pref_title_notification_alert_sound">ئاگادارکردنەوەی بە دەنگێک</string> + <string name="pref_title_notification_alerts">ئاگادارکردنەوەکان</string> + <string name="pref_title_notifications_enabled">ئاگانامەکان</string> + <string name="pref_title_edit_notification_settings">ئاگانامەکان</string> + <string name="visibility_direct">ڕاستەوخۆ: تەنها بۆ بەکارهێنەرانی ناوبراو پۆست بکە</string> + <string name="visibility_private">تەنها شوێنکەوتوانی: تەنها پۆست بۆ شوێنکەوتوانی</string> + <string name="visibility_unlisted">لیستی نەکراو: لە هێڵی کاتی گشتی دا پیشان مەدە</string> + <string name="visibility_public">گشتی: پۆست بکە بۆ هێڵی کاتی گشتی</string> + <string name="dialog_mute_hide_notifications">شاردنەوەی ئاگانامەکان</string> + <string name="dialog_mute_warning">بێدەنگکردن @%1$s؟</string> + <string name="dialog_block_warning">بلۆککردنی @%1$s؟</string> + <string name="mute_domain_warning_dialog_ok">شاردنەوەی هەموو دۆمەینەکە</string> + <string name="mute_domain_warning">ئایا دڵنیایت لەوەی دەتەوێت هەموو %1$s بلۆک بکەیت؟ تۆ ناوەڕۆکێک نابینیت لە دۆمەینەکە لە هیچ هێڵی کاتی گشتی یان لە ئاگانامەکانت. شوێنکەوتوانی تۆ لەو دۆمەینەوە لادەبرێن.</string> + <string name="dialog_redraft_post_warning">ئەم دووانە بسڕەوە و دووبارە ڕەشنووس یان دەکەیتەوە؟</string> + <string name="dialog_delete_post_warning">ئەم توتە بسڕەوە؟</string> + <string name="dialog_unfollow_warning">شوێن نەکەوتنی ئەم هەژمارە؟</string> + <string name="dialog_message_cancel_follow_request">داواکاری بەدوادا چوەکان هەڵوەشانەوە؟</string> + <string name="dialog_download_image">داگرتن</string> + <string name="dialog_message_uploading_media">بارکردن…</string> + <string name="dialog_title_finishing_media_upload">تەواوکردنی بارکردنی میدیا</string> + <string name="dialog_whats_an_instance">ناونیشان یان دۆمەینی هەر نمونەیەک دەکرێت لێرە تێبنووسرێت، وەک <a href="https://instances.social">فرەتر!</a> +\n +\nئەگەر هێشتا ئەژمێرێکت نیە، دەتوانیت ناوی ئەو نمونەیە داخڵ بکەیت کە دەتەوێت بیبەستیت و ئەژمێرێک دروست بکەیت لەوێ. +\n +\nنموونەیەک تاکە شوێنە کە ئەژمێرەکەت میوانداری کراوە، بەڵام دەتوانیت بە ئاسانی پەیوەندی لەگەڵ بکەیت و دوای ئەو خەڵکانە بکەویت لە نمونەکانی تر وەک ئەوەی تۆ لە هەمان سایت دابیت. +\n +\nزانیاری زیاتر دەتوانرێت بدۆزرێتەوە لە <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="login_connection">گرێدان…</string> + <string name="link_whats_an_instance">نموونەیەک چییە؟</string> + <string name="label_header">سەرپەڕە</string> + <string name="label_avatar">وێنۆچکە</string> + <string name="label_quick_reply">وەڵام…</string> + <string name="search_no_results">هیچ ئەنجامێک نیە</string> + <string name="hint_search">گەڕان…</string> + <string name="hint_note">دەربارە</string> + <string name="hint_display_name">ناوی پیشاندان</string> + <string name="hint_content_warning">ئاگاداری ناوەڕۆک</string> + <string name="action_mute_conversation">گفتوگۆی بێدەنگ</string> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d کاژێرماوە</item> + <item quantity="other">%1$d کاژێرماوە</item> + </plurals> + <string name="filter_dialog_whole_word_description">کاتێک وشەکە یان دەستەواژەکە تەنها ئەبجەدییە، تەنها ئەگەر لەگەڵ هەموو وشەکە یەکبێت کاری پێدەکرێت</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="other">ناتوانیت زیاتر لە %1$d هاوپێچی میدیا باربکەیت.</item> + </plurals> + <string name="wellbeing_hide_stats_profile">شاردنەوەی زانیاری چەندێتی لەسەر پرۆفایلەکان</string> + <string name="wellbeing_hide_stats_posts">شاردنەوەی زانیاری چەندێتی لە بابەتەکان</string> + <string name="limit_notifications">سنووردارکردنی ئاگانامەکانی تایم لاین</string> + <string name="review_notifications">پێداچوونەوەی ئاگانامەکان</string> + <string name="wellbeing_mode_notice">هەندێک زانیاری کە لەوانەیە کاریگەری لەسەر باشبوونی دەروونیت دروست بکات دەشاردرێنەوە. ئەمە پێکدێت لە: +\n +\n- ئاگانامەکانی پەسەند/بەهێزکردن/بەدوادا +\n - پەسەندترین/بەرزکردنەوە لەسەر توت +\n - بەدواداچوون/زانیاری بابەت لەسەر پرۆفایلەکان +\n +\nکارتێکردنی ئاگانامەکانی پاڵپێوەنان، بەڵام دەتوانیت بە پەسەندکردنە ئاگانامەکانت دا بخشێنیەوە بە دەستی.</string> + <string name="account_note_saved">ڕزگارکرا</string> + <string name="account_note_hint">تێبینیی تایبەتیت بۆ ئەم هەژمارە</string> + <string name="pref_title_wellbeing_mode">Wellbeing</string> + <string name="pref_title_hide_top_toolbar">شاردنەوەی ناونیشانی شریتی ئامڕازی سەرەوە</string> + <string name="pref_title_confirm_reblogs">پیشاندانی دیالۆگی دووپاتکردنەوە پێش بەهێزکردن</string> + <string name="pref_title_show_cards_in_timelines">نیشاندانی پێشاندانی بەستەر لە هێڵی کات</string> + <string name="warning_scheduling_interval">ماستۆدۆن کەمترین ماوەی خشتەی هەیە لە ٥ خولەک.</string> + <string name="no_announcements">هیچ ڕاگه یه نراوێک له بەرده رنه کەون.</string> + <string name="no_scheduled_posts">هیچ بارێکی خشتەکراوت نیە.</string> + <string name="no_drafts">هیچ ڕەشنووسێکت نییە.</string> + <string name="post_lookup_error_format">هەڵە لە گەڕان بەدوای بابەت %1$s</string> + <string name="edit_poll">دەستکاریکردن</string> + <string name="poll_new_choice_hint">هەڵبژاردنی %1$d</string> + <string name="poll_allow_multiple_choices">چەند هەڵبژاردنێک</string> + <string name="add_poll_choice">زیادکردنی هەڵبژاردن</string> + <string name="create_poll_title">ڕاپرسی</string> + <string name="pref_title_enable_swipe_for_tabs">چالاککردنی ئاماژەکردنی لێدانی چالاک بۆ گۆڕین لە نێوان خشتەبەندەکان</string> + <string name="license_description">تاسکی کۆد و سەرمایەکانی تێدایە لەم پڕۆژە کراوەی سەرچاوە:</string> + <string name="failed_search">گەڕانەکە سەرکەوتوو نەبوو</string> + <string name="title_accounts">ئەژمێرەکان</string> + <string name="report_description_remote_instance">هەژمارەلە ڕاژەیەکی دیکەیە ترە. کۆپیەکی بێ سەروبەر بنێرە بۆ ڕاپۆرتەکە لەوێ؟</string> + <string name="report_description_1">ڕاپۆرتەکە دەنێردرێت بۆ بەڕێوەبەری ڕاژەکەت. دەتوانیت ڕوونکردنەوەیەک پێشکەش بکەیت کە بۆچی ئەم ئەژمێرە لە خوارەوە ڕاپۆرت دەکەیت:</string> + <string name="failed_fetch_posts">سەرکەوتوو نەبوو لە هێنانی بارەکان</string> + <string name="failed_report">ڕاپۆرتکردن سەرکەوتوو نەبوو</string> + <string name="report_remote_instance">ناردنەوە بۆ %1$s</string> + <string name="hint_additional_info">سەرنجەکانی زیاتر</string> + <string name="report_sent_success">سەرکەوتووانە ڕاپۆرتکرا @%1$s</string> + <string name="button_done">تەواوبوو</string> + <string name="button_back">دواوە</string> + <string name="button_continue">بەردەوام بە</string> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d چرکەی ماوەو</item> + <item quantity="other">%1$d دووەم چەپ</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d خولەک ماوە</item> + <item quantity="other">%1$d خولەک ماوە</item> + </plurals> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d ڕۆژ ماوە</item> + <item quantity="other">%1$d ڕۆژ ماوە</item> + </plurals> + <string name="poll_ended_created">ڕاپرسییەک کە دروستت کردووە کۆتایی هات</string> + <string name="poll_ended_voted">ڕاپرسییەک کە دەنگی پێداویت کۆتایی هات</string> + <string name="poll_vote">دەنگ</string> + <string name="poll_info_closed">کۆتایی هاتووە</string> + <string name="poll_info_time_absolute">کۆتایی دێت لە %1$s</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s کەس</item> + <item quantity="other">%1$s کەس</item> + </plurals> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s دەنگ</item> + <item quantity="other">%1$s دەنگ</item> + </plurals> + <string name="poll_info_format"> <!-- 15 دەنگ • 1 کاتژمێر ماوە --> %1$s • %2$s</string> + <string name="compose_preview_image_description">کارەکان بۆ وێنە %1$s</string> + <string name="notification_clear_text">ئایا دڵنیایت لەوەی دەتەوێت بە هەمیشەیی هەموو ئاگانامەکانت بسڕیتەوە؟</string> + <string name="compose_shortcut_short_label">دروستکردن</string> + <string name="compose_shortcut_long_label">دروستکردنی توت</string> + <string name="filter_apply">جێبەجێ کردن</string> + <string name="notifications_apply_filter">فلتەر</string> + <string name="notifications_clear">سڕینەوە</string> + <string name="list">لیست</string> + <string name="select_list_title">دیاریکردنی لیست</string> + <string name="hashtags">هاشتاگی</string> + <string name="edit_hashtag_hint">هاشتاگی بێ #</string> + <string name="add_hashtag_title">هاشتاگی زیاد بکە</string> + <string name="hint_list_name">ناوی لیست</string> + <string name="description_poll">ڕاپرسی لەگەڵ هەڵبژاردنەکان: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="description_visibility_direct">ڕاستەوخۆ</string> + <string name="description_visibility_private">شوێنکەوتوانی</string> + <string name="description_visibility_unlisted">لە لیست نەکراو</string> + <string name="description_visibility_public">گشتی</string> + <string name="description_post_bookmarked">نیشانکراوە</string> + <string name="description_post_favourited">پەسەندکراو</string> + <string name="description_post_reblogged">دووبارە بڵاگ کرا</string> + <string name="description_post_media_no_description_placeholder">هیچ وەسفێک</string> + <string name="description_post_cw">ئاگاداری ناوەڕۆک: %1$s</string> + <string name="description_post_media">میدیا: %1$s</string> + <string name="conversation_more_recipients">%1$s, %2$s و %3$d زیاتر</string> + <string name="conversation_2_recipients">%1$s و %2$s</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="title_favourited_by">پەسەندکراوە لەلایەن</string> + <string name="title_reblogged_by">پۆست کراوەتەوە لەلایەن</string> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> پۆستکردنەوە</item> + <item quantity="other"><b>%1$s</b> پۆستکردنەوە</item> + </plurals> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> دڵخواز</item> + <item quantity="other"><b>%1$s</b> دڵخواز</item> + </plurals> + <string name="pin_action">Pin</string> + <string name="unpin_action">لابردن</string> + <string name="label_remote_account">ڕەنگە زانیاری خوارەوە ڕەنگدانەوەی پرۆفایلی بەکارهێنەر بە ناتەواوی بێت. فشار بکە بۆ کردنەوەی پرۆفایلی تەواو لە وێبگەڕەکە.</string> + <string name="pref_title_absolute_time">کاتی ڕەها بەکاربهێنە</string> + <string name="profile_metadata_content_label">ناوەڕۆک</string> + <string name="profile_metadata_label_label">ناونیشان</string> + <string name="profile_metadata_add">داتا زیاد بکە</string> + <string name="profile_metadata_label">مێتاداتای پرۆفایل</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_apache_2">مۆڵەتدراوە لەژێر مۆڵەتی ئەپاچی (لەبەرگیراوە لە خوارەوە)</string> + <string name="unreblog_private">بێ هێزکردن</string> + <string name="reblog_private">بەرزکردنەوە بۆ جەماوەری ڕەسەن</string> + <string name="account_moved_description">%1$s گواسترایەوە بۆ:</string> + <string name="profile_badge_bot_text">بۆت</string> + <string name="download_failed">داگرتن سەرکەوتوو نەبوو</string> + <string name="caption_notoemoji">کۆمەڵە ئیمۆجیەکەی ئێستای گووگڵ</string> + <string name="caption_twemoji">سێتی ئیمۆجی پێوانەیی ماتۆدۆن</string> + <string name="caption_blobmoji">ئیمۆجی Blob لە ئەندرۆید ەوە ناسراوە 4.4–7.1</string> + <string name="caption_systememoji">سێتی ئیمۆجی بنەڕەتی ئامێرەکەت</string> + <string name="restart">دەستپێکردنەوە</string> + <string name="later">دواتر</string> + <string name="restart_emoji">تۆ پێویستە توسکی دەستپێبکەیتەوە بۆ ئەوەی ئەم گۆڕانکاریانە جێبەجێ بکەیت</string> + <string name="restart_required">دەسپێکردنەوەی کاربەرنامە پێویستە</string> + <string name="action_open_post">کردنەوە توت</string> + <string name="expand_collapse_all_posts">فراوانکردن/نوشتانەوەی هەموو بارەکان</string> + <string name="performing_lookup_title">ئەنجامدانی گەڕان…</string> + <string name="download_fonts">تۆ پێویستە سەرەتا ئەم سێتە ئیمۆجییانە دابگریت</string> + <string name="system_default">سیستەمی بنەڕەت</string> + <string name="emoji_style">شێوازی ئیمۆجی</string> + <string name="error_no_custom_emojis">نموونەکەت %1$s هیچ ئیمۆجییەکی ئاسایی نییە</string> + <string name="action_compose_shortcut">دروستکردن</string> + <string name="send_post_notification_saved_content">کۆپیەکی دەستنووسەکە خەزن کراوە بۆ ڕەشنووسەکانت</string> + <string name="send_post_notification_cancel_title">ناردنی هەڵوەشاوە</string> + <string name="send_post_notification_channel_name">ناردنی توتس</string> + <string name="send_post_notification_error_title">هەڵە لە ناردنی توت</string> + <string name="send_post_notification_title">(توت) دەنێرم…</string> + <string name="compose_save_draft">ڕەشنووس پاشەکەوت بکەیت؟</string> + <string name="lock_account_label_description">داوات لێدەکات کە بە دەستی شوێنکەوتوانی پەسەند بکە</string> + <string name="lock_account_label">داخستنی ئەژمێر</string> + <string name="action_remove">لابردن</string> + <string name="action_set_caption">دانانی سەردێڕ</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">وەسف بکە بۆ بینایی داڕماو +\n(%1$d سنوری کاراکتەر)</item> + </plurals> + <string name="compose_active_account_description">بڵاوکردنەوە بە هەژماری %1$s</string> + <string name="action_remove_from_list">لابردنی ئەژمێر لە لیستەکە</string> + <string name="action_add_to_list">زیادکردنی ئەژمێر بۆ لیستەکە</string> + <string name="hint_search_people_list">گەڕان بەدوای ئەو کەسانەی کە پەیڕەوی ان دەکەیت</string> + <string name="action_delete_list">سڕینەوەی لیستەکە</string> + <string name="action_rename_list">ناونانەوەی لیستەکە</string> + <string name="action_create_list">دروستکردنی لیستێک</string> + <string name="error_delete_list">نەیتوانی لیستەکە بسڕێتەوە</string> + <string name="error_rename_list">نەیتوانی ناوی لیست بنووسرێ</string> + <string name="error_create_list">نەیتوانی لیست دروست بکات</string> + <string name="title_lists">لیستەکان</string> + <string name="action_lists">لیستەکان</string> + <string name="add_account_description">زیادکردنی ئەژمێری ماتۆدۆنی نوێ</string> + <string name="add_account_name">زیادکردنی ئەژمێر</string> + <string name="filter_add_description">دەستەواژە بۆ فلتەر</string> + <string name="filter_dialog_whole_word">هەموو وشەکە</string> + <string name="filter_dialog_update_button">نوێکردنەوە</string> + <string name="filter_dialog_remove_button">لابردن</string> + <string name="filter_edit_title">دەستکاریکردنی فلتەر</string> + <string name="filter_addition_title">زیادکردنی فلتەر</string> + <string name="pref_title_thread_filter_keywords">گفتوگۆکان</string> + <string name="pref_title_public_filter_keywords">هێڵی کاتی گشتی</string> + <string name="load_more_placeholder_text">بارکردنی زیاتر</string> + <string name="replying_to">وەڵام دانەوە بۆ @%1$s</string> + <string name="title_media">میدیا</string> + <string name="pref_title_alway_open_spoiler">هەمیشە ئەو توتانەی کە بە ئاگادارکردنەوەکانی ناوەڕۆکەوە نیشانەکراون فراوان بکە</string> + <string name="pref_title_alway_show_sensitive_media">هەمیشە ناوەڕۆکی هەستیار نیشان بدە</string> + <string name="follows_you">دوای تۆ دەکەوێت</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_years_ago">%1$dy</string> + <string name="abbreviated_in_seconds">لە %1$ds</string> + <string name="abbreviated_in_minutes">لە %1$dm</string> + <string name="abbreviated_in_hours">لە %1$dh</string> + <string name="abbreviated_in_days">لە %1$dd</string> + <string name="abbreviated_in_years">لە %1$dy</string> + <string name="state_follow_requested">بەدواداچوونەوەی داواکراو</string> + <string name="post_media_video">ڤیدیۆ</string> + <string name="post_media_images">وێنەکان</string> + <string name="post_share_link">هاوبەشکردنی لینک بۆ توت</string> + <string name="post_share_content">هاوبەشکردنی ناوەڕۆکی دووت</string> + <string name="about_tusky_account">پرۆفایلی تاسکی</string> + <string name="about_bug_feature_request_site">ڕاپۆرتەکانی هەڵەکان و داواکاریەکانی تایبەتمەندی: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_project_site">وێبسایتی پڕۆژە: +\nhttps://tusky.app</string> + <string name="about_tusky_license">توسکی سۆفتوێری ئازاد و سەرچاوەی کراوەیە مۆڵەتدراوە بە پێ نامەی گشتی GNU Public Version 3. دەتوانیت لێرە مۆڵەتەکە نیشان بدەی: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="about_powered_by_tusky">لەلایەن تاسکیەوە دەست کراوە بە</string> + <string name="about_tusky_version">توسکی %1$s</string> + <string name="about_title_activity">سەبارەت</string> + <string name="description_account_locked">هەژماری داخراو</string> + <plurals name="notification_title_summary"> + <item quantity="other">%1$d چالاکی نوێ</item> + </plurals> + <string name="notification_summary_small">%1$s و %2$s</string> + <string name="notification_summary_medium">%1$s و %2$s و %3$s</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s و %4$d ئەوانی تر</string> + <string name="notification_mention_format">%1$s ئاماژەی بە تۆ کرد</string> + <string name="notification_subscription_description">ئاگانامەکان کاتێک کەسێک کە تۆ بەشداریت کردووە لە بڵاوکردنەوەی توتێکی نوێ</string> + <string name="notification_subscription_name">توتی نوێ</string> + <string name="notification_poll_description">ئاگادارییەکان دەربارەی ڕاپرسییەکان کە کۆتایی هاتووە</string> + <string name="notification_poll_name">ڕاپرسییەکان</string> + <string name="notification_favourite_description">ئاگانامەکان کاتێک کەتوتەکان نیشانە کراون وەک دڵخواز</string> + <string name="notification_favourite_name">دڵخوازەکان</string> + <string name="notification_boost_description">ئاگانامەکان کاتێک کە دووتەکەت بەرز دەکرێتەوە</string> + <string name="notification_boost_name">بەهێزکردن</string> + <string name="notification_follow_request_description">ئاگانامەکان دەربارەی داواکاریەکانی بەدوادا</string> + <string name="notification_follow_request_name">بەدواداچونی داواکاریەکان بکە</string> + <string name="notification_follow_description">ئاگانامەکان دەربارەی شوێنکەوتوانی نوێ</string> + <string name="notification_follow_name">شوێنکەوتوانی نوێ</string> + <string name="notification_mention_descriptions">ئاگانامەکان دەربارەی ئاماژە نوێیەکان</string> + <string name="notification_mention_name">ئاماژە نوێیەکان</string> + <string name="post_text_size_largest">گەورەترین</string> + <string name="post_text_size_large">گەورە</string> + <string name="post_text_size_medium">مامناوەندی</string> + <string name="post_text_size_small">بچووک</string> + <string name="post_text_size_smallest">بچووکترین</string> + <string name="pref_post_text_size">قەبارەی دەقی بار</string> + <string name="post_privacy_followers_only">شوێنکەوتوانی تەنها</string> + <string name="post_privacy_unlisted">لە لیست نەکراو</string> + <string name="post_privacy_public">گشتی</string> + <string name="pref_main_nav_position_option_bottom">خوارەوە</string> + <string name="pref_main_nav_position_option_top">سەرەوە</string> + <string name="pref_main_nav_position">شوێنی سەرەکی ڕێنیشاندەر</string> + <string name="pref_failed_to_sync">سەرکەوتوو نەبوو لە هاودەمکردنی ڕێکبەندەکان</string> + <string name="pref_publishing">بڵاوکردنەوە (هاوکاتکراوە لەگەڵ سێرڤەر)</string> + <string name="pref_default_media_sensitivity">هەمیشە میدیا وەک هەستیار نیشان بکە</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml new file mode 100644 index 0000000..7bb1ed4 --- /dev/null +++ b/app/src/main/res/values-cs/strings.xml @@ -0,0 +1,573 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Vyskytla se chyba.</string> + <string name="error_network">Vyskytla se chyba sítě! Prosím zkontrolujte své připojení a zkuste to znovu!</string> + <string name="error_empty">Toto nemůže být prázdné.</string> + <string name="error_invalid_domain">Byla zadána neplatná doména</string> + <string name="error_failed_app_registration">Autentizace s tímto serverem nebyla úspěšná.</string> + <string name="error_no_web_browser_found">Nepodařilo se najít webový prohlížeč, který lze použít.</string> + <string name="error_authorization_unknown">Vyskytla se neidentifikovaná chyba autorizace.</string> + <string name="error_authorization_denied">Autorizace byla zamítnuta.</string> + <string name="error_retrieving_oauth_token">Nepodařilo se získat přihlašovací token.</string> + <string name="error_compose_character_limit">Příspěvek je příliš dlouhý!</string> + <string name="error_media_upload_type">Tento typ souboru nemůže být nahrán.</string> + <string name="error_media_upload_opening">Tento soubor se nepodařilo otevřít.</string> + <string name="error_media_upload_permission">Je vyžadováno oprávnění ke čtení médií.</string> + <string name="error_media_download_permission">Je vyžadováno oprávnění ukládat média.</string> + <string name="error_media_upload_image_or_video">K jednomu příspěvku nemohou být přiloženy obrázky i videa.</string> + <string name="error_media_upload_sending">Nahrání se nezdařilo.</string> + <string name="error_sender_account_gone">Chyba při odesílání příspěvku.</string> + <string name="title_home">Domů</string> + <string name="title_notifications">Oznámení</string> + <string name="title_public_local">Místní</string> + <string name="title_public_federated">Federované</string> + <string name="title_direct_messages">Přímé zprávy</string> + <string name="title_tab_preferences">Panely</string> + <string name="title_view_thread">Vlákno</string> + <string name="title_posts">Příspěvky</string> + <string name="title_posts_with_replies">S odpověďmi</string> + <string name="title_posts_pinned">Připnuté</string> + <string name="title_follows">Sledovaní</string> + <string name="title_followers">Sledující</string> + <string name="title_favourites">Oblíbené</string> + <string name="title_mutes">Skrytí uživatelé</string> + <string name="title_blocks">Blokovaní uživatelé</string> + <string name="title_follow_requests">Žádosti o sledování</string> + <string name="title_edit_profile">Upravit váš profil</string> + <string name="title_drafts">Koncepty</string> + <string name="title_licenses">Licence</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s boostnul/a</string> + <string name="post_sensitive_media_title">Citlivý obsah</string> + <string name="post_media_hidden_title">Média skryta</string> + <string name="post_sensitive_media_directions">Klikněte pro zobrazení</string> + <string name="post_content_warning_show_more">Zobrazit více</string> + <string name="post_content_warning_show_less">Zobrazit méně</string> + <string name="post_content_show_more">Rozbalit</string> + <string name="post_content_show_less">Sbalit</string> + <string name="message_empty">Tady nic není.</string> + <string name="footer_empty">Tady nic není. Obnovte přetažením dolů!</string> + <string name="notification_reblog_format">%1$s boostnul/a váš příspěvek</string> + <string name="notification_favourite_format">%1$s si oblíbil/a váš příspěvek</string> + <string name="notification_follow_format">%1$s vás nyní sleduje</string> + <string name="report_username_format">Nahlásit uživatele @%1$s</string> + <string name="report_comment_hint">Další komentáře\?</string> + <string name="action_quick_reply">Rychlá odpověď</string> + <string name="action_reply">Odpovědět</string> + <string name="action_reblog">Boostnout</string> + <string name="action_unreblog">Odstranit boost</string> + <string name="action_favourite">Oblíbit</string> + <string name="action_unfavourite">Odstranit oblíbení</string> + <string name="action_more">Více</string> + <string name="action_compose">Napsat</string> + <string name="action_login">Přihlásit se účtem Mastodon</string> + <string name="action_logout">Odhlásit se</string> + <string name="action_logout_confirm">Jste si jistý/á, že se chcete odhlásit z účtu %1$s?</string> + <string name="action_follow">Sledovat</string> + <string name="action_unfollow">Přestat sledovat</string> + <string name="action_block">Blokovat</string> + <string name="action_unblock">Odblokovat</string> + <string name="action_hide_reblogs">Skrýt boosty</string> + <string name="action_show_reblogs">Zobrazit boosty</string> + <string name="action_report">Nahlásit</string> + <string name="action_delete">Smazat</string> + <string name="action_send">TOOTNOUT</string> + <string name="action_send_public">TOOTNOUT!</string> + <string name="action_retry">Zkusit znovu</string> + <string name="action_close">Zavřít</string> + <string name="action_view_profile">Profil</string> + <string name="action_view_preferences">Předvolby</string> + <string name="action_view_account_preferences">Předvolby účtu</string> + <string name="action_view_favourites">Oblíbené</string> + <string name="action_view_mutes">Skrytí uživatelé</string> + <string name="action_view_blocks">Blokovaní uživatelé</string> + <string name="action_view_follow_requests">Žádosti o sledování</string> + <string name="action_view_media">Média</string> + <string name="action_open_in_web">Otevřít v prohlížeči</string> + <string name="action_add_media">Přidat média</string> + <string name="action_photo_take">Pořídit fotku</string> + <string name="action_share">Sdílet</string> + <string name="action_mute">Skrýt</string> + <string name="action_unmute">Zrušit skrytí</string> + <string name="action_mention">Zmínit</string> + <string name="action_hide_media">Skrýt média</string> + <string name="action_open_drawer">Otevřít menu</string> + <string name="action_save">Uložit</string> + <string name="action_edit_profile">Upravit profil</string> + <string name="action_edit_own_profile">Upravit</string> + <string name="action_undo">Vrátit</string> + <string name="action_accept">Přijmout</string> + <string name="action_reject">Zamítnout</string> + <string name="action_search">Hledat</string> + <string name="action_access_drafts">Koncepty</string> + <string name="action_toggle_visibility">Viditelnost příspěvku</string> + <string name="action_content_warning">Varování o obsahu</string> + <string name="action_emoji_keyboard">Klávesnice s emoji</string> + <string name="action_add_tab">Přidat panel</string> + <string name="action_links">Odkazy</string> + <string name="action_mentions">Zmínky</string> + <string name="action_hashtags">Hashtagy</string> + <string name="action_open_reblogger">Otevřít autora boostu</string> + <string name="action_open_reblogged_by">Zobrazit boosty</string> + <string name="action_open_faved_by">Zobrazit oblíbení</string> + <string name="title_hashtags_dialog">Hashtagy</string> + <string name="title_mentions_dialog">Zmínky</string> + <string name="title_links_dialog">Odkazy</string> + <string name="action_open_media_n">Otevřít médium #%1$d</string> + <string name="download_image">Stahuji %1$s</string> + <string name="action_copy_link">Zkopírovat odkaz</string> + <string name="action_open_as">Otevřít jako %1$s</string> + <string name="action_share_as">Sdílet jako…</string> + <string name="download_media">Stáhnout média</string> + <string name="downloading_media">Stahuji média</string> + <string name="send_post_link_to">Sdílet URL příspěvku na…</string> + <string name="send_post_content_to">Sdílet příspěvek na…</string> + <string name="send_media_to">Sdílet média na…</string> + <string name="confirmation_reported">Odesláno!</string> + <string name="confirmation_unblocked">Uživatel byl odblokován</string> + <string name="confirmation_unmuted">Skrytí uživatele bylo zrušeno</string> + <string name="hint_domain">Který server?</string> + <string name="hint_compose">Co se právě děje?</string> + <string name="hint_content_warning">Varování o obsahu</string> + <string name="hint_display_name">Zobrazované jméno</string> + <string name="hint_note">O vás</string> + <string name="hint_search">Hledat…</string> + <string name="search_no_results">Žádné výsledky</string> + <string name="label_quick_reply">Odpovědět…</string> + <string name="label_avatar">Avatar</string> + <string name="label_header">Záhlaví</string> + <string name="link_whats_an_instance">Co je to server\?</string> + <string name="login_connection">Připojuji se…</string> + <string name="dialog_whats_an_instance">Sem může být zadána adresa či doména jakéhokoliv + serveru, například mastodon.social, icosahedron.website, social.tchncs.de + a <a href="https://instances.social">další!</a> + \n\nPokud ještě nemáte účet, můžete zadat název instance, ke které se chcete + připojit, a vytvořit si tam účet.\n\nServer je jedno místo, kde je hostován váš + účet, můžete však jednoduše komunikovat a sledovat lidi na jiných serverech, jako by + byli na stejné stránce. + \n\nDalší informace mohou být nalezeny na stránce <a href="https://joinmastodon.org">joinmastodon.org</a>. + </string> + <string name="dialog_title_finishing_media_upload">Dokončuji nahrávání médií</string> + <string name="dialog_message_uploading_media">Nahrávám…</string> + <string name="dialog_download_image">Stáhnout</string> + <string name="dialog_message_cancel_follow_request">Zrušit požadavek o sledování?</string> + <string name="dialog_unfollow_warning">Přestat sledovat tento účet?</string> + <string name="dialog_delete_post_warning">Smazat tento příspěvek\?</string> + <string name="visibility_public">Veřejný: Poslat na veřejné časové osy</string> + <string name="visibility_unlisted">Neuvedený: Neposlat na veřejné časové osy</string> + <string name="visibility_private">Pouze pro sledující: Poslat pouze sledujícím</string> + <string name="visibility_direct">Přímé: Poslat pouze zmíněným uživatelům</string> + <string name="pref_title_edit_notification_settings">Oznámení</string> + <string name="pref_title_notifications_enabled">Oznámení</string> + <string name="pref_title_notification_alerts">Upozornění</string> + <string name="pref_title_notification_alert_sound">Oznamovat se zvukem</string> + <string name="pref_title_notification_alert_vibrate">Oznamovat s vibrací</string> + <string name="pref_title_notification_alert_light">Oznamovat se světlem</string> + <string name="pref_title_notification_filters">Oznamovat, když</string> + <string name="pref_title_notification_filter_mentions">jsem zmíněn/a</string> + <string name="pref_title_notification_filter_follows">jsem sledován/a</string> + <string name="pref_title_notification_filter_reblogs">jsou moje příspěvky boostnuty</string> + <string name="pref_title_notification_filter_favourites">jsou moje příspěvky oblíbeny</string> + <string name="pref_title_appearance_settings">Vzhled</string> + <string name="pref_title_app_theme">Motiv aplikace</string> + <string name="pref_title_timelines">Časové osy</string> + <string name="pref_title_timeline_filters">Filtry</string> + <string name="app_them_dark">Tmavý</string> + <string name="app_theme_light">Světlý</string> + <string name="app_theme_black">Černý</string> + <string name="app_theme_auto">Automaticky při západu slunce</string> + <string name="app_theme_system">Použít systémový design</string> + <string name="pref_title_browser_settings">Prohlížeč</string> + <string name="pref_title_custom_tabs">Používat Vlastní karty Chrome</string> + <string name="pref_title_language">Jazyk</string> + <string name="pref_title_post_filter">Filtrování časových os</string> + <string name="pref_title_post_tabs">Panely</string> + <string name="pref_title_show_boosts">Zobrazit boosty</string> + <string name="pref_title_show_replies">Zobrazit odpovědi</string> + <string name="pref_title_show_media_preview">Stahovat náhledy médií</string> + <string name="pref_title_proxy_settings">Proxy</string> + <string name="pref_title_http_proxy_settings">HTTP proxy</string> + <string name="pref_title_http_proxy_enable">Povolit HTTP proxy</string> + <string name="pref_title_http_proxy_server">HTTP proxy server</string> + <string name="pref_title_http_proxy_port">HTTP proxy port</string> + <string name="pref_default_post_privacy">Výchozí soukromí příspěvků</string> + <string name="pref_default_media_sensitivity">Vždy označovat média jako citlivá</string> + <string name="pref_publishing">Publikování (synchronizováno se serverem)</string> + <string name="pref_failed_to_sync">Nepodařilo se synchronizovat nastavení</string> + <string name="post_privacy_public">Veřejné</string> + <string name="post_privacy_unlisted">Neuvedené</string> + <string name="post_privacy_followers_only">Pouze pro sledující</string> + <string name="pref_post_text_size">Velikost textu příspěvků</string> + <string name="post_text_size_smallest">Nejmenší</string> + <string name="post_text_size_small">Malý</string> + <string name="post_text_size_medium">Střední</string> + <string name="post_text_size_large">Velký</string> + <string name="post_text_size_largest">Největší</string> + <string name="notification_mention_name">Nové zmínky</string> + <string name="notification_mention_descriptions">Oznámení o nových zmínkách</string> + <string name="notification_follow_name">Noví sledující</string> + <string name="notification_follow_description">Oznámení o nových sledujících</string> + <string name="notification_boost_name">Boosty</string> + <string name="notification_boost_description">Oznámení, když jsou vaše příspěvky boostnuty</string> + <string name="notification_favourite_name">Oblíbení</string> + <string name="notification_favourite_description">Oznámení, když jsou vaše příspěvky označeny jako oblíbené</string> + <string name="notification_mention_format">%1$s vás zmínil/a</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s a dalších %4$d</string> + <string name="notification_summary_medium">%1$s, %2$s a %3$s</string> + <string name="notification_summary_small">%1$s a %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d nová interakce</item> + <item quantity="few">%1$d nové interakce</item> + <item quantity="other">%1$d nových interakcí</item> + </plurals> + <string name="description_account_locked">Uzamčený účet</string> + <string name="about_title_activity">O této aplikaci</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tusky je svobodný a otevřený software. + Je dostupný pod licencí GNU General Public License, verze 3. + Licenci můžete zobrazit zde: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site"> Webová stránka projektu:\n + https://tusky.app + </string> + <string name="about_bug_feature_request_site"> Hlášení chyb a návrhy na nové vlastnosti:\n + https://github.com/tuskyapp/Tusky/issues + </string> + <string name="about_tusky_account">Profil aplikace Tusky</string> + <string name="post_share_content">Sdílet obsah příspěvku</string> + <string name="post_share_link">Sdílet odkaz na příspěvek</string> + <string name="post_media_images">Obrázky</string> + <string name="post_media_video">Video</string> + <string name="state_follow_requested">Vyžádáno sledování</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">za %1$d let</string> + <string name="abbreviated_in_days">za %1$d d</string> + <string name="abbreviated_in_hours">za %1$d h</string> + <string name="abbreviated_in_minutes">za %1$d m</string> + <string name="abbreviated_in_seconds">za %1$d s</string> + <string name="abbreviated_years_ago">%1$d let</string> + <string name="abbreviated_days_ago">%1$d d</string> + <string name="abbreviated_hours_ago">%1$d h</string> + <string name="abbreviated_minutes_ago">%1$d min</string> + <string name="abbreviated_seconds_ago">%1$d s</string> + <string name="follows_you">Sleduje vás</string> + <string name="pref_title_alway_show_sensitive_media">Vždy zobrazovat citlivý obsah</string> + <string name="title_media">Média</string> + <string name="replying_to">Odpověď uživateli @%1$s</string> + <string name="load_more_placeholder_text">načíst více</string> + <string name="pref_title_public_filter_keywords">Veřejné časové osy</string> + <string name="pref_title_thread_filter_keywords">Konverzace</string> + <string name="filter_addition_title">Přidat filtr</string> + <string name="filter_edit_title">Upravit filtr</string> + <string name="filter_dialog_remove_button">Odstranit</string> + <string name="filter_dialog_update_button">Aktualizovat</string> + <string name="filter_add_description">Fráze k filtrování</string> + <string name="add_account_name">Přidat účet</string> + <string name="add_account_description">Přidat nový účet Mastodon</string> + <string name="action_lists">Seznamy</string> + <string name="title_lists">Seznamy</string> + <string name="error_create_list">Nelze vytvořit seznam</string> + <string name="error_rename_list">Nelze přejmenovat seznam</string> + <string name="error_delete_list">Nelze smazat seznam</string> + <string name="action_create_list">Vytvořit seznam</string> + <string name="action_rename_list">Přejmenovat seznam</string> + <string name="action_delete_list">Smazat seznam</string> + <string name="hint_search_people_list">Hledejte mezi lidmi, které sledujete</string> + <string name="action_add_to_list">Přidat účet na seznam</string> + <string name="action_remove_from_list">Odstranit účet ze seznamu</string> + <string name="compose_active_account_description">Píšete jako %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Popis pro zrakově postižené +\n(limit %1$d znak)</item> + <item quantity="few">Popis pro zrakově postižené +\n(limit %1$d znaky)</item> + <item quantity="other">Popis pro zrakově postižené +\n(limit %1$d znaků)</item> + </plurals> + <string name="action_set_caption">Nastavit popisek</string> + <string name="action_remove">Odstranit</string> + <string name="lock_account_label">Uzamknout účet</string> + <string name="lock_account_label_description">Vyžaduje, abyste ručně schvaloval/a sledující</string> + <string name="compose_save_draft">Uložit koncept?</string> + <string name="send_post_notification_title">Odesílám příspěvek…</string> + <string name="send_post_notification_error_title">Chyba při odesílání příspěvku</string> + <string name="send_post_notification_channel_name">Odesílám příspěvky</string> + <string name="send_post_notification_cancel_title">Odesílání bylo zrušeno</string> + <string name="send_post_notification_saved_content">Kopie vašeho příspěvku byla uložena do vašich konceptů</string> + <string name="action_compose_shortcut">Napsat</string> + <string name="error_no_custom_emojis">Vaše instance %1$s nemá žádná vlastní emoji</string> + <string name="emoji_style">Styl emoji</string> + <string name="system_default">Výchozí nastavení systému</string> + <string name="download_fonts">Musíte si nejprve stáhnout tyto sady emoji</string> + <string name="performing_lookup_title">Provádím prohledávání…</string> + <string name="expand_collapse_all_posts">Rozbalit/Sbalit všechny příspěvky</string> + <string name="action_open_post">Otevřít příspěvek</string> + <string name="restart_required">Je vyžadováno restartování aplikace</string> + <string name="restart_emoji">Pro použití těchto změn musíte restartovat aplikaci Tusky</string> + <string name="later">Později</string> + <string name="restart">Restartovat</string> + <string name="caption_systememoji">Výchozí sada emoji vašeho zařízení</string> + <string name="caption_blobmoji">Blob Emoji známá z Androidu 4.4–7.1</string> + <string name="caption_twemoji">Standardní sada emoji na Mastodonu</string> + <string name="download_failed">Stahování selhalo</string> + <string name="profile_badge_bot_text">Robot</string> + <string name="account_moved_description">%1$s se přesunul/a na:</string> + <string name="reblog_private">Boostnout původnímu publiku</string> + <string name="unreblog_private">Zrušit boost</string> + <string name="license_description">Tusky obsahuje kód a zdroje z následujících otevřených projektů:</string> + <string name="license_apache_2">Pod licencí Apache License (kopie níže)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Metadata profilu</string> + <string name="profile_metadata_add">přidat data</string> + <string name="profile_metadata_label_label">Označení</string> + <string name="profile_metadata_content_label">Obsah</string> + <string name="pref_title_absolute_time">Používat absolutní čas</string> + <string name="label_remote_account">Níže uvedené informace nemusejí zcela odrážet profil uživatele. Dotknutím se otevřete celý profil v prohlížeči.</string> + <string name="unpin_action">Odepnout</string> + <string name="pin_action">Připnout</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> oblíbení</item> + <item quantity="few"><b>%1$s</b> oblíbení</item> + <item quantity="other"><b>%1$s</b> oblíbení</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> boost</item> + <item quantity="few"><b>%1$s</b> boosty</item> + <item quantity="other"><b>%1$s</b> boostů</item> + </plurals> + <string name="title_reblogged_by">Boostnuto uživatelem</string> + <string name="title_favourited_by">Oblíbeno uživatelem</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s a %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s a %3$d další</string> + <string name="description_post_media">Média %1$s</string> + <string name="description_post_cw">Varování o obsahu: %1$s</string> + <string name="description_post_media_no_description_placeholder">Žádný popis</string> + <string name="description_post_reblogged">Boostnutý</string> + <string name="description_post_favourited">Oblíbený</string> + <string name="description_visibility_public">Veřejný</string> + <string name="description_visibility_unlisted">Neuvedený</string> + <string name="description_visibility_private">Pro sledující</string> + <string name="description_visibility_direct">Přímý</string> + <string name="hint_list_name">Název seznamu</string> + <string name="edit_hashtag_hint">Hashtag bez #</string> + <string name="compose_shortcut_long_label">Napsat příspěvek</string> + <string name="compose_shortcut_short_label">Napsat</string> + <string name="notifications_clear">Vyčistit</string> + <string name="notifications_apply_filter">Filtrovat</string> + <string name="filter_apply">Použít</string> + <string name="pref_title_bot_overlay">Zobrazovat indikátor pro roboty</string> + <string name="notification_clear_text">Jste si jistý/á, že chcete trvale vymazat všechna vaše oznámení\?</string> + <string name="action_delete_and_redraft">Smazat a přepsat</string> + <string name="dialog_redraft_post_warning">Smazat a přepsat tento příspěvek\?</string> + <string name="poll_info_format"> <!-- 15 hlasů • 1 hodina do konce --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s hlas</item> + <item quantity="few">%1$s hlasy</item> + <item quantity="other">%1$s hlasů</item> + </plurals> + <string name="poll_info_time_absolute">končí v %1$s</string> + <string name="poll_info_closed">uzavřena</string> + <string name="poll_vote">Hlasovat</string> + <string name="pref_title_notification_filter_poll">skončí ankety</string> + <string name="notification_poll_name">Ankety</string> + <string name="notification_poll_description">Oznámení o anketách, které skončily</string> + <string name="compose_preview_image_description">Akce pro obrázek %1$s</string> + <string name="poll_ended_voted">Anketa, ve které jste hlasoval/a, skončila</string> + <string name="poll_ended_created">Anketa, kterou jste vytvořil/a, skončila</string> + <plurals name="poll_timespan_days"> + <item quantity="one">zbývá %1$d den</item> + <item quantity="few">zbývá %1$d dny</item> + <item quantity="other">zbývá %1$d dní</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">zbývá %1$d minuta</item> + <item quantity="few">zbývá %1$d minuty</item> + <item quantity="other">zbývá %1$d minut</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">zbývá %1$d sekunda</item> + <item quantity="few">zbývá %1$d sekundy</item> + <item quantity="other">zbývá %1$d sekund</item> + </plurals> + <string name="pref_title_animate_gif_avatars">Animovat GIF avatary</string> + <string name="description_poll">Anketa s volbami: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="title_domain_mutes">Skryté domény</string> + <string name="action_view_domain_mutes">Skryté domény</string> + <string name="action_mute_domain">Skrýt doménu %1$s</string> + <string name="confirmation_domain_unmuted">Skrytí domény %1$s bylo zrušeno</string> + <string name="mute_domain_warning">Jste si jistý/á, že chcete zablokovat vše z domény %1$s\? Z této domény neuvidíte obsah v žádné veřejné časové ose ani v oznámeních. Vaši sledující z této domény budou odstraněni.</string> + <string name="mute_domain_warning_dialog_ok">Skrýt celou doménu</string> + <string name="caption_notoemoji">Aktuální sada emoji od Googlu</string> + <string name="button_continue">Pokračovat</string> + <string name="button_back">Zpět</string> + <string name="button_done">Hotovo</string> + <string name="report_sent_success">\@%1$s byla/a úspěšně nahlášen/a</string> + <string name="hint_additional_info">Další komentáře</string> + <string name="report_remote_instance">Přeposlat na %1$s</string> + <string name="failed_report">Nahlášení selhalo</string> + <string name="failed_fetch_posts">Stahování příspěvků selhalo</string> + <string name="report_description_1">Nahlášení bude zasláno moderátorovi vašeho serveru. Níže můžete uvést, proč tento účet nahlašujete:</string> + <string name="report_description_remote_instance">Tento účet je z jiného serveru. Chcete na něj také poslat anonymizovanou kopii nahlášení\?</string> + <string name="create_poll_title">Anketa</string> + <string name="duration_5_min">5 minut</string> + <string name="duration_30_min">30 minut</string> + <string name="duration_1_hour">1 hodina</string> + <string name="duration_6_hours">6 hodin</string> + <string name="duration_1_day">1 den</string> + <string name="duration_3_days">3 dny</string> + <string name="duration_7_days">7 dní</string> + <string name="add_poll_choice">Přidat možnost</string> + <string name="poll_allow_multiple_choices">Lze zvolit více možností</string> + <string name="poll_new_choice_hint">Možnost %1$d</string> + <string name="edit_poll">Upravit</string> + <string name="title_scheduled_posts">Naplánováné příspěvky</string> + <string name="action_edit">Upravit</string> + <string name="action_add_poll">Přidat anketu</string> + <string name="action_access_scheduled_posts">Naplánované příspěvky</string> + <string name="action_schedule_post">Naplánované příspěvky</string> + <string name="action_reset_schedule">Obnovit</string> + <string name="pref_title_alway_open_spoiler">Vždy rozbalovat příspěvky označené varováními o obsahu</string> + <string name="filter_dialog_whole_word">Celé slovo</string> + <string name="filter_dialog_whole_word_description">Je-li klíčové slovo nebo fráze pouze alfanumerická, bude použita pouze, pokud odpovídá celému slovu</string> + <string name="title_accounts">Účty</string> + <string name="failed_search">Hledání selhalo</string> + <string name="post_lookup_error_format">Chyba při hledání příspěvku %1$s</string> + <string name="notification_follow_request_description">Upozornění na žádosti o sledování</string> + <string name="notification_follow_request_name">Žádosti o sledování</string> + <string name="pref_main_nav_position_option_bottom">Dole</string> + <string name="pref_title_gradient_for_media">Ukázat barevné čtverečky místo skrytých médií</string> + <string name="dialog_mute_hide_notifications">Skrýt notifikace</string> + <string name="action_unmute_conversation">Zrušit ztlumení konverzace</string> + <string name="action_mute_conversation">Ztlumit konverzaci</string> + <string name="action_view_bookmarks">Záložky</string> + <string name="action_bookmark">Záložka</string> + <string name="title_bookmarks">Záložky</string> + <string name="pref_title_show_cards_in_timelines">Ukazovat náhledy k odkazům</string> + <string name="warning_scheduling_interval">Mastodon neumožňuje pracovat s intervalem menším než 5 minut.</string> + <string name="no_scheduled_posts">Zatím zde nemáte žádné naplánované statusy.</string> + <string name="no_drafts">Zatím zde nemáte žádné koncepty.</string> + <string name="pref_title_enable_swipe_for_tabs">Možnost přetahování prstem pro přechod mezi kartami</string> + <string name="list">Seznam</string> + <string name="add_hashtag_title">Přidat hashtag</string> + <string name="description_post_bookmarked">Uloženo do Záložek</string> + <string name="select_list_title">Vybrat seznam</string> + <string name="pref_main_nav_position">Umístění hlavní navigační lišty</string> + <string name="hashtags">Hashtagy</string> + <string name="about_powered_by_tusky">Powered by Tusky</string> + <string name="dialog_block_warning">Zablokovat @%1$s\?</string> + <string name="pref_main_nav_position_option_top">Nahoře</string> + <string name="action_unmute_domain">Zrušit skrytí domény %1$s</string> + <string name="action_unmute_desc">Zrušit skrytí %1$s</string> + <string name="dialog_mute_warning">Skrýt @%1$s\?</string> + <string name="notification_follow_request_format">%1$s požádal/a aby vás mohl/a sledovat</string> + <string name="pref_title_confirm_reblogs">Zobrazit dialogové okno s potvrzením při boostování</string> + <string name="notification_subscription_format">%1$s právě zveřejnil/a příspěvek</string> + <string name="title_announcements">Oznámení</string> + <string name="title_login">Přihlášení</string> + <string name="notification_sign_up_format">%1$s se zaregistroval/a</string> + <string name="title_migration_relogin">Přihlaste se znovu pro push oznámení</string> + <string name="error_could_not_load_login_page">Nepodařilo se načíst přihlášovací stránku.</string> + <string name="drafts_post_failed_to_send">Tento příspěvek se nepodařilo odeslat!</string> + <string name="error_loading_account_details">Nepodařilo se načíst detaily účtu</string> + <string name="drafts_failed_loading_reply">Nepodařilo se načíst informace o odpovědi</string> + <string name="error_image_edit_failed">Obrázek se nepodařilo upravit.</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s osoba</item> + <item quantity="few">%1$s lidi</item> + <item quantity="other">%1$s lidí</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">zbývá %1$d hodina</item> + <item quantity="few">zbývají %1$d hodiny</item> + <item quantity="other">zbývá %1$d hodin</item> + </plurals> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Nemůžete nahrát více než %1$d mediální přílohu.</item> + <item quantity="few">Nemůžete nahrát více než %1$d mediální přílohy.</item> + <item quantity="other">Nemůžete nahrát více než %1$d mediálních příloh.</item> + </plurals> + <string name="delete_scheduled_post_warning">Smazat tento naplánovaný příspěvek\?</string> + <string name="notification_subscription_description">Upozornění na nový toot někoho, koho sledujete.</string> + <string name="instance_rule_info">Přihlášením souhlasíte s pravidly serveru %1$s.</string> + <string name="instance_rule_title">Pravidla serveru %1$s</string> + <string name="wellbeing_mode_notice">Některé informace, které mohou ovlivnit Vaši duševní pohodu, mohou být skryty. To zahrnuje: +\n +\n - Upozornění na boosty, oblíbené a sledování +\n - Počty boostů a oblíbení u příspěvků +\n - Statistiky sledujících a příspěvků na profilech +\n +\nPush oznámení nebudou ovlivněna, ale můžete si zkontrolovat jejich nastavení manuálně.</string> + <string name="dialog_push_notification_migration_other_accounts">Znovu jste se přihlásili ke svému aktuálnímu účtu, abyste aplikaci Tusky udělili oprávnění k odběru push. Stále však máte další účty, které tímto způsobem migrovány nebyly. Přepněte se na ně a znovu se přihlaste na jednom po druhém, abyste povolili podporu oznámení UnifiedPush.</string> + <string name="compose_save_draft_loses_media">Uložit koncept\? (Přílohy budou znovu nahrány, když obnovíte koncept.)</string> + <string name="set_focus_description">Klepnutím nebo přetažením kruhu vyberte ohnisko, které bude vždy viditelné v miniaturách.</string> + <string name="pref_title_confirm_favourites">Před oblíbením zobrazit dialog pro potvrzení</string> + <string name="pref_title_hide_top_toolbar">Skrýt nadpis horního panelu nástrojů</string> + <string name="wellbeing_hide_stats_profile">Skrýt kvantitativní statistiky profilů</string> + <string name="dialog_delete_list_warning">Opravdu chcete smazat seznam %1$s\?</string> + <string name="follow_requests_info">I když váš účet není uzamčen, zaměstnanci %1$s si myslí, že byste mohli chtít zkontrolovat žádosti o sledování z těchto účtů ručně.</string> + <string name="action_subscribe_account">Odebírat</string> + <string name="action_unsubscribe_account">Přestat odebírat</string> + <string name="tusky_compose_post_quicksetting_label">Vytvořit příspěvek</string> + <string name="draft_deleted">Koncept byl smazán</string> + <string name="error_multimedia_size_limit">Video a audio soubory nesmí překročit velikost %1$s MB.</string> + <string name="failed_to_pin">Připnutí se nezdařilo</string> + <string name="failed_to_unpin">Zrušení připnutí se nezdařilo</string> + <string name="pref_show_self_username_always">Vždy</string> + <string name="pref_show_self_username_never">Nikdy</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="action_edit_image">Upravit obrázek</string> + <string name="duration_14_days">14 dní</string> + <string name="duration_30_days">30 dní</string> + <string name="drafts_post_reply_removed">Příspěvek, na který jste připravili odpověď, byl odstraněn</string> + <string name="action_set_focus">Nastavit bod zaostření</string> + <string name="tips_push_notification_migration">Znovu se přihlaste ke všem účtům, abyste povolili podporu push oznámení.</string> + <string name="dialog_push_notification_migration">Aby bylo možné používat push oznámení prostřednictvím UnifiedPush, Tusky potřebuje oprávnění k odběru oznámení na vašem serveru Mastodon. To vyžaduje opětovné přihlášení ke změně rozsahů OAuth udělených aplikaci Tusky. Použitím možnosti opětovného přihlášení zde nebo v předvolbách účtu zachováte všechny vaše místní koncepty a mezipaměť.</string> + <string name="action_add_reaction">přidat reakci</string> + <string name="pref_title_notification_filter_updates">příspěvek, se kterým jsem interagoval/a, je upraven</string> + <string name="pref_title_notification_filter_sign_ups">někdo se zaregistroval</string> + <string name="pref_title_notification_filter_subscriptions">někdo, ke komu jsem přihlášen/a, zveřejnil nový příspěvek</string> + <string name="pref_show_self_username_disambiguate">Když je přihlášeno více účtů</string> + <string name="notification_subscription_name">Nové příspěvky</string> + <string name="notification_sign_up_name">Registrace</string> + <string name="notification_sign_up_description">Oznámení o nových uživatelích</string> + <string name="notification_update_name">Úpravy příspěvků</string> + <string name="notification_update_description">Oznámení, když je upraven příspěvek, se kterým jste interagovala, je upraven</string> + <string name="post_media_audio">Audio</string> + <string name="post_media_attachments">Přílohy</string> + <string name="status_count_one_plus">1+</string> + <string name="description_post_language">Jazyk příspěvku</string> + <string name="label_duration">Doba trvání</string> + <string name="duration_indefinite">Na neurčito</string> + <string name="duration_60_days">60 dní</string> + <string name="duration_90_days">90 dní</string> + <string name="duration_180_days">180 dní</string> + <string name="duration_365_days">365 dní</string> + <string name="duration_no_change">(Beze změny)</string> + <string name="no_announcements">Nejsou zde žádná oznámení.</string> + <string name="pref_title_show_self_username">Zobrazit uživatelské jméno v panelech nástrojů</string> + <string name="account_note_hint">Váše soukromá poznámka o tomto účtu.</string> + <string name="account_note_saved">Uloženo!</string> + <string name="pref_title_wellbeing_mode">Pohoda</string> + <string name="review_notifications">Zkontrolovat oznámení</string> + <string name="limit_notifications">Omezit upozornění na časové ose</string> + <string name="wellbeing_hide_stats_posts">Skrýt kvantitativní statistiky příspěvků</string> + <string name="account_date_joined">Připojil/a se %1$s</string> + <string name="saving_draft">Koncept se ukládá…</string> + <string name="error_following_hashtag_format">Chyba při sledování #%1$s</string> + <string name="error_unfollowing_hashtag_format">Chyba při rušení sledování #%1$s</string> + <string name="notification_update_format">%1$s upravil/a svůj příspěvek</string> + <string name="action_unbookmark">Odebrat záložku</string> + <string name="action_delete_conversation">Smazat konverzaci</string> + <string name="action_dismiss">Zavřít</string> + <string name="action_details">Podrobnosti</string> + <string name="dialog_delete_conversation_warning">Smazat tuto konverzaci\?</string> + <string name="pref_title_notification_filter_follow_requests">Požádáno o sledování</string> + <string name="pref_title_animate_custom_emojis">Animovat vlastní emotikony</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-cy/strings.xml b/app/src/main/res/values-cy/strings.xml new file mode 100644 index 0000000..4290c1e --- /dev/null +++ b/app/src/main/res/values-cy/strings.xml @@ -0,0 +1,767 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Digwyddodd gwall.</string> + <string name="error_empty">Gall hwn ddim fod yn wag.</string> + <string name="error_invalid_domain">Wedi cynnig parth annilys</string> + <string name="error_failed_app_registration">Wedi methu ag awdurdodi gyda\'r gweinydd hwnnw. Os bydd hyn yn parhau, ceisiwch Mewngofnodi yn y Porwr o\'r ddewislen.</string> + <string name="error_no_web_browser_found">Methu dod o hyd i borwr gwe i\'w ddefnyddio.</string> + <string name="error_authorization_unknown">Bu gwall awdurdodi anhysbys. Os bydd hyn yn parhau, ceisiwch Mewngofnodi yn y Porwr o\'r ddewislen.</string> + <string name="error_authorization_denied">Gwrthodwyd awdurdodi. Os ydych yn siŵr eich bod chi wedi gyflenwi\'r manylion cywir, ceisiwch Mewngofnodi yn y Porwr o\'r ddewislen.</string> + <string name="error_retrieving_oauth_token">Wedi methu cael tocyn mewngofnodi. Os bydd hyn yn parhau, ceisiwch Mewngofnodi yn y Porwr o\'r ddewislen.</string> + <string name="error_compose_character_limit">Mae\'r neges yn rhy hir!</string> + <string name="error_media_upload_type">Nid oes modd llwytho\'r math yma o ffeil.</string> + <string name="error_media_upload_opening">Nid oedd modd agor y ffeil honno.</string> + <string name="error_media_upload_permission">Rhaid cael caniatâd i ddarllen y cyfrwng hwn.</string> + <string name="error_media_download_permission">Rhaid cael caniatâd i gadw\'r cyfrwng hwn.</string> + <string name="error_media_upload_image_or_video">Ni allwch chi atodi delweddau a fideos i\'r un neges.</string> + <string name="error_media_upload_sending">Methodd llwytho i fyny.</string> + <string name="error_sender_account_gone">Bu gwall wrth anfon y neges.</string> + <string name="title_home">Hafan</string> + <string name="title_notifications">Hysbysiadau</string> + <string name="title_public_local">Lleol</string> + <string name="title_public_federated">Ffedereiddiwyd</string> + <string name="title_view_thread">Edefyn</string> + <string name="title_posts">Negeseuon</string> + <string name="title_posts_with_replies">Gydag ymatebion</string> + <string name="title_follows">Yn dilyn</string> + <string name="title_followers">Dilynwyr</string> + <string name="title_favourites">Ffefrynnau</string> + <string name="title_mutes">Defnyddwyr wedi\'u tewi</string> + <string name="title_blocks">Defnyddwyr wedi\'u rhwystro</string> + <string name="title_follow_requests">Ceisiadau i\'ch dilyn</string> + <string name="title_edit_profile">Golygu eich proffil</string> + <string name="title_drafts">Drafftiau</string> + <string name="title_licenses">Trwyddedau</string> + <string name="post_boosted_format">Hybodd %1$s</string> + <string name="post_sensitive_media_title">Cynnwys sensitif</string> + <string name="post_media_hidden_title">Cyfryngau cudd</string> + <string name="post_sensitive_media_directions">Cliciwch i weld</string> + <string name="post_content_warning_show_more">Dangos Mwy</string> + <string name="post_content_warning_show_less">Dangos Llai</string> + <string name="post_content_show_more">Chwyddo</string> + <string name="post_content_show_less">Lleihau</string> + <string name="footer_empty">Dim byd yma. Tynnwch lawr i adnewyddu!</string> + <string name="notification_reblog_format">Hybodd %1$s eich neges</string> + <string name="notification_favourite_format">Hoffodd %1$s eich neges</string> + <string name="notification_follow_format">Mae %1$s wedi\'ch dilyn chi</string> + <string name="report_username_format">Adrodd @%1$s</string> + <string name="report_comment_hint">Sylwadau ychwanegol?</string> + <string name="action_quick_reply">Ymateb yn gyflym</string> + <string name="action_reply">Ymateb</string> + <string name="action_reblog">Hybu</string> + <string name="action_favourite">Hoffi</string> + <string name="action_more">Mwy</string> + <string name="action_compose">Creu</string> + <string name="action_login">Mewngofnodi â Tusky</string> + <string name="action_logout">Allgofnodi</string> + <string name="action_logout_confirm">Ydych chi\'n siŵr eich bod am allgofnodi o\'r cyfrif %1$s? Bydd hyn yn dileu\'r holl ddata lleol, gan gynnwys drafftiau a dewisiadau.</string> + <string name="action_follow">Dilyn</string> + <string name="action_unfollow">Dad-ddilyn</string> + <string name="action_block">Rhwystro</string> + <string name="action_unblock">Dadrwystro</string> + <string name="action_hide_reblogs">Cuddio hybiau</string> + <string name="action_show_reblogs">Dangos hybiau</string> + <string name="action_report">Adrodd</string> + <string name="action_delete">Dileu</string> + <string name="action_send">TŴTIO</string> + <string name="action_send_public">TŴTIO!</string> + <string name="action_retry">Ceisio eto</string> + <string name="action_close">Cau</string> + <string name="action_view_profile">Proffil</string> + <string name="action_view_preferences">Dewisiadau</string> + <string name="action_view_favourites">Ffefrynnau</string> + <string name="action_view_mutes">Defnyddwyr wedi\'u tewi</string> + <string name="action_view_blocks">Defnyddwyr wedi\'u rhwystro</string> + <string name="action_view_follow_requests">Ceisiadau i\'ch dilyn</string> + <string name="action_view_media">Cyfryngau</string> + <string name="action_open_in_web">Agor mewn porwr</string> + <string name="action_add_media">Ychwanegu cyfryngau</string> + <string name="action_photo_take">Tynnu llun</string> + <string name="action_share">Rhannu</string> + <string name="action_mute">Tewi</string> + <string name="action_unmute">Dad-dewi</string> + <string name="action_mention">Crybwyll</string> + <string name="action_hide_media">Cuddio cyfrwng</string> + <string name="action_open_drawer">Agor drôr</string> + <string name="action_save">Cadw</string> + <string name="action_edit_profile">Golygu\'ch proffil</string> + <string name="action_edit_own_profile">Golygu</string> + <string name="action_undo">Dadwneud</string> + <string name="action_accept">Derbyn</string> + <string name="action_reject">Gwrthod</string> + <string name="action_search">Chwilio</string> + <string name="action_access_drafts">Drafftiau</string> + <string name="action_toggle_visibility">Gwelededd y neges</string> + <string name="action_content_warning">Rhybudd cynnwys</string> + <string name="action_emoji_keyboard">Bysellfwrdd emoji</string> + <string name="download_image">Yn lawrlwytho %1$s</string> + <string name="action_copy_link">Copïo\'r ddolen</string> + <string name="send_post_link_to">Rhannu URL y neges i…</string> + <string name="send_post_content_to">Rhannu\'r neges i…</string> + <string name="send_media_to">Rhannu\'r cyfryngau i…</string> + <string name="confirmation_reported">Anfonwyd!</string> + <string name="confirmation_unblocked">Dadrwystrwyd y defnyddiwr</string> + <string name="confirmation_unmuted">Dad-dawyd defnyddiwr</string> + <string name="hint_domain">Pa weinydd\?</string> + <string name="hint_compose">Beth sy\'n digwydd?</string> + <string name="hint_content_warning">Rhybudd cynnwys</string> + <string name="hint_display_name">Enw dangos</string> + <string name="hint_note">Amdanaf</string> + <string name="hint_search">Chwilio…</string> + <string name="search_no_results">Dim canlyniadau</string> + <string name="label_quick_reply">Ymateb…</string> + <string name="label_avatar">Llun proffil</string> + <string name="label_header">Pennyn</string> + <string name="link_whats_an_instance">Beth yw gweinydd\?</string> + <string name="login_connection">Yn cysylltu…</string> + <string name="dialog_whats_an_instance">Gallwch chi roi cyfeiriad neu barth o unrhyw weinydd yma, fel mastodon.social, twt.cymru, social.tchncs.de, a <a href="https://instances.social">mwy!</a> \u0020 +\n \u0020 +\nOs nad oes gennych chi gyfrif, gallwch chi roi enw\'r gweinydd yr hoffech chi ymuno ag ef a chreu cyfrif yno. \u0020 +\n \u0020 +\nGweinydd yw\'r man y mae\'ch gyfrif wedi\'i gynnal, ond gallwch chi gyfathrebu\'n hawdd â phobl a\'u dilyn ar weinyddion eraill fel petaech yn yr unfan. \u0020 +\n \u0020 +\nRhagor o wybodaeth yn <a href="https://joinmastodon.org">joinmastodon.org</a>. \u0020</string> + <string name="dialog_title_finishing_media_upload">Yn Gorffen Llwytho\'r Cyfryngau i Fyny</string> + <string name="dialog_message_uploading_media">Yn llwytho i fyny…</string> + <string name="dialog_download_image">Lawrlwytho</string> + <string name="dialog_message_cancel_follow_request">Gwrthod y cais i\'ch dilyn chi\?</string> + <string name="dialog_unfollow_warning">Dad-ddilyn y cyfrif hwn?</string> + <string name="dialog_delete_post_warning">Dileu\'r neges hon\?</string> + <string name="visibility_public">Cyhoeddus: Postio i ffrydiau cyhoeddus</string> + <string name="visibility_unlisted">Heb restru: Peidio â dangos ar ffrydiau cyhoeddus</string> + <string name="visibility_private">Dilynwyr yn unig: Postio at ddilynwyr yn unig</string> + <string name="visibility_direct">Uniongyrchol: Postio at ddefnyddwyr wedi\'u crybwyll yn unig</string> + <string name="pref_title_edit_notification_settings">Hysbysiadau</string> + <string name="pref_title_notifications_enabled">Hysbysiadau</string> + <string name="pref_title_notification_alerts">Rhybuddion</string> + <string name="pref_title_notification_alert_sound">Hysbysu â sain</string> + <string name="pref_title_notification_alert_vibrate">Hysbysiad drwy grynu</string> + <string name="pref_title_notification_alert_light">Hysbysu â golau</string> + <string name="pref_title_notification_filters">Roi gwybod i mi pryd</string> + <string name="pref_title_notification_filter_mentions">crybwyllwyd</string> + <string name="pref_title_notification_filter_follows">dilynodd</string> + <string name="pref_title_notification_filter_reblogs">mae fy negeseuon yn cael eu hybu</string> + <string name="pref_title_notification_filter_favourites">mae fy negeseuon yn cael eu hoffi</string> + <string name="pref_title_appearance_settings">Gwedd</string> + <string name="pref_title_app_theme">Thema\'r ap</string> + <string name="app_them_dark">Tywyll</string> + <string name="app_theme_light">Golau</string> + <string name="app_theme_black">Du</string> + <string name="app_theme_auto">Awtomatig wrth iddi nosi</string> + <string name="pref_title_browser_settings">Porwr</string> + <string name="pref_title_custom_tabs">Defnyddio Tabiau Cyfaddas Chrome</string> + <string name="pref_title_post_filter">Hidlo ffrydiau</string> + <string name="pref_title_post_tabs">Ffrwd gartref</string> + <string name="pref_title_show_boosts">Dangos hybiau</string> + <string name="pref_title_show_replies">Dangos ymatebion</string> + <string name="pref_title_show_media_preview">Dangos rhagolwg cyfryngau</string> + <string name="pref_title_proxy_settings">Dirprwy</string> + <string name="pref_title_http_proxy_settings">Dirprwy HTTP</string> + <string name="pref_title_http_proxy_enable">Galluogi dirprwy HTTP</string> + <string name="pref_title_http_proxy_server">Gweinydd dirprwy HTTP</string> + <string name="pref_title_http_proxy_port">Porth y dirprwy HTTP</string> + <string name="pref_default_post_privacy">Preifatrwydd rhagosodedig negeseuon (bydd yn cael ei gydweddu gyda\'ch gweinydd)</string> + <string name="pref_publishing">Cyhoeddi</string> + <string name="post_privacy_public">Cyhoeddus</string> + <string name="post_privacy_unlisted">Heb ei restru</string> + <string name="post_privacy_followers_only">Dilynwyr yn unig</string> + <string name="pref_post_text_size">Maint testun negeseuon</string> + <string name="post_text_size_smallest">Lleiaf</string> + <string name="post_text_size_small">Bach</string> + <string name="post_text_size_medium">Canolig</string> + <string name="post_text_size_large">Mawr</string> + <string name="post_text_size_largest">Mwyaf</string> + <string name="notification_mention_name">Crybwylliadau newydd</string> + <string name="notification_mention_descriptions">Hysbysiadau am grybwylliadau newydd</string> + <string name="notification_follow_name">Dilynwyr newydd</string> + <string name="notification_follow_description">Hysbysiadau am ddilynwyr newydd </string> + <string name="notification_boost_name">Hybiau</string> + <string name="notification_boost_description">Hysbysiadau pan gaiff eich negeseuon eu hybu</string> + <string name="notification_favourite_name">Ffefrynnau</string> + <string name="notification_favourite_description">Hysbysiadau pan gaiff eich negeseuon eu marcio fel ffefryn</string> + <string name="notification_mention_format">Crybwyllodd %1$s chi</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s a %4$d eraill</string> + <string name="notification_summary_medium">%1$s, %2$s, a %3$s</string> + <string name="notification_summary_small">%1$s a %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="zero">%1$d rhyngweithiadau newydd</item> + <item quantity="one">%1$d rhyngweithiad newydd</item> + <item quantity="two">%1$d ryngweithiad newydd</item> + <item quantity="few">%1$d rhyngweithiad newydd</item> + <item quantity="many">%1$d rhyngweithiad newydd</item> + <item quantity="other">%1$d rhyngweithiad newydd</item> + </plurals> + <string name="description_account_locked">Cyfrif wedi\'i gloi</string> + <string name="about_title_activity">Ynghylch</string> + <string name="about_tusky_license">Mae Tusky yn feddalwedd cod-agored ac am ddim. Mae\'n cael ei drwyddedu o dan Drwydded Cyhoeddus Cyffredinol GNU Fersiwn 3. Gallwch chi weld y drwydded yma: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">Gwefan y project: https://tusky.app</string> + <string name="about_bug_feature_request_site">Adrodd ar wallau a cheisiadau nodweddion: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Proffil Tusky</string> + <string name="post_share_content">Rhannu cynnwys y neges</string> + <string name="post_share_link">Rhannu dolen i\'r neges</string> + <string name="post_media_images">Delweddau</string> + <string name="post_media_video">Fideo</string> + <string name="state_follow_requested">Ceisiwyd</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">ymhen %1$db</string> + <string name="abbreviated_in_days">ymhen %1$dd</string> + <string name="abbreviated_in_hours">ymhen %1$da</string> + <string name="abbreviated_in_minutes">ymhen %1$dm</string> + <string name="abbreviated_in_seconds">ymhen %1$de</string> + <string name="abbreviated_years_ago">%1$db yn ôl</string> + <string name="abbreviated_days_ago">%1$dd yn ôl</string> + <string name="abbreviated_hours_ago">%1$da yn ôl</string> + <string name="abbreviated_minutes_ago">%1$dm yn ôl</string> + <string name="abbreviated_seconds_ago">%1$de yn ôl</string> + <string name="follows_you">Yn eich dilyn chi</string> + <string name="pref_title_alway_show_sensitive_media">Dangos cynnwys sensitif bob tro</string> + <string name="title_media">Cyfryngau</string> + <string name="replying_to">Yn ymateb i @%1$s</string> + <string name="load_more_placeholder_text">llwytho rhagor</string> + <string name="add_account_name">Ychwanegu cyfrif</string> + <string name="add_account_description">Ychwanegu cyfrif Mastodon newydd</string> + <string name="action_lists">Rhestrau</string> + <string name="title_lists">Rhestrau</string> + <string name="compose_active_account_description">Yn postio fel %1$s</string> + <string name="action_set_caption">Gosod pennawd</string> + <string name="action_remove">Dileu</string> + <string name="lock_account_label">Cloi cyfrif</string> + <string name="lock_account_label_description">Bydd angen cymeradwyo eich dilynwyr eich hunan</string> + <string name="compose_save_draft">Cadw drafft?</string> + <string name="send_post_notification_title">Yn anfon y neges…</string> + <string name="send_post_notification_error_title">Gwall wrth anfon y neges</string> + <string name="send_post_notification_channel_name">Yn anfon negeseuon</string> + <string name="send_post_notification_cancel_title">Diddymwyd anfon</string> + <string name="send_post_notification_saved_content">Cadwyd copi o\'r neges i\'ch drafftiau</string> + <string name="action_compose_shortcut">Creu</string> + <string name="error_no_custom_emojis">Nid oes gan eich gweinydd %1$s emojis personol</string> + <string name="emoji_style">Arddull emoji</string> + <string name="system_default">Rhagosodiad system</string> + <string name="download_fonts">Bydd angen i chi lawrlwytho\'r setiau emoji hyn yn gyntaf</string> + <string name="performing_lookup_title">Wrthi\'n chwilio…</string> + <string name="expand_collapse_all_posts">Chwyddo/lleihau pob neges</string> + <string name="action_open_post">Agor y neges</string> + <string name="restart_required">Rhaid ailddechrau\'r ap</string> + <string name="restart_emoji">Bydd angen ailddechrau Tusky i roi\'r newidiadau ar waith</string> + <string name="later">Yn ddiweddarach</string> + <string name="restart">Ailddechrau</string> + <string name="caption_systememoji">Set emoji ragosodedig eich dyfais</string> + <string name="caption_blobmoji">Daw\'r emoji Blob o Android 4.4–7.1</string> + <string name="caption_twemoji">Set emoji safonol Mastodon</string> + <string name="download_failed">Methodd y lawrlwytho</string> + <string name="account_moved_description">Mae %1$s wedi symud i:</string> + <string name="reblog_private">Hybu i\'r gynulleidfa wreiddiol</string> + <string name="unreblog_private">Dad-hybu</string> + <string name="license_description">Mae Tusky yn cynnwys cod ac asedau o\'r projectau cod agored canlynol:</string> + <string name="license_apache_2">Yn cael ei drwyddedu o Drwydded Apache (copi isod)</string> + <string name="profile_metadata_label">Metadata\'r proffil</string> + <string name="profile_metadata_add">ychwanegu data</string> + <string name="profile_metadata_content_label">Cynnwys</string> + <string name="pref_title_absolute_time">Defnyddio amser absoliwt</string> + <string name="post_username_format">\@%1$s</string> + <string name="error_network">Digwyddodd gwall rhwydwaith. Gwiriwch eich cysylltiad a cheisiwch eto.</string> + <string name="title_direct_messages">Negeseuon uniongyrchol</string> + <string name="title_tab_preferences">Tabiau</string> + <string name="title_posts_pinned">Wedi\'i binio</string> + <string name="title_domain_mutes">Parthau cudd</string> + <string name="message_empty">Dim byd yma.</string> + <string name="action_unreblog">Dileu hwb</string> + <string name="action_unfavourite">Dileu ffefryn</string> + <string name="action_delete_and_redraft">Dileu ac ailddrafftio</string> + <string name="action_view_account_preferences">Dewisiadau\'ch cyfrif</string> + <string name="action_view_domain_mutes">Parthau cudd</string> + <string name="action_mute_domain">Tewi %1$s</string> + <string name="action_add_tab">Ychwanegu tab</string> + <string name="action_links">Dolenni</string> + <string name="title_links_dialog">Dolenni</string> + <string name="action_open_reblogged_by">Dangos hybiau</string> + <string name="notification_follow_request_name">Ceisiadau i\'ch dilyn</string> + <string name="action_bookmark">Tudalnodi</string> + <string name="action_edit">Golygu</string> + <string name="edit_poll">Golygu</string> + <string name="compose_shortcut_short_label">Creu</string> + <string name="description_visibility_private">Dilynwyr</string> + <string name="description_visibility_unlisted">Heb ei restru</string> + <string name="conversation_2_recipients">%1$s a %2$s</string> + <string name="filter_dialog_remove_button">Dileu</string> + <string name="description_visibility_public">Cyhoeddus</string> + <string name="action_unmute_conversation">Dad-dewi\'r sgwrs</string> + <string name="pref_title_thread_filter_keywords">Sgyrsiau</string> + <string name="mute_domain_warning_dialog_ok">Cuddio parth i gyd</string> + <string name="action_mute_conversation">Tewi\'r sgwrs</string> + <string name="notifications_apply_filter">Hidlo</string> + <string name="notification_poll_description">Hysbysiadau am bolau sydd wedi\'u cwblhau</string> + <string name="account_date_joined">Ymunodd %1$s</string> + <string name="no_scheduled_posts">Does gennych chi ddim negeseuon wedi\'u hamserlennu.</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="notification_clear_text">Ydych chi\'n siŵr eich bod chi am glirio\'ch holl hysbysiadau yn barhaol\?</string> + <string name="error_multimedia_size_limit">Nid yw ffeiliau fideo a sain yn gallu bod yn fwy na %1$s MB.</string> + <string name="error_following_hashtag_format">Gwall wrth ddilyn #%1$s</string> + <string name="error_unfollowing_hashtag_format">Gwall wrth ddad-ddilyn #%1$s</string> + <string name="action_unmute_domain">Dad-dewi %1$s</string> + <string name="notification_subscription_format">Postiodd %1$s</string> + <string name="action_delete_conversation">Dileu\'r sgwrs</string> + <string name="action_view_bookmarks">Tudalnodau</string> + <string name="action_add_poll">Ychwanegu pôl</string> + <string name="confirmation_domain_unmuted">%1$s wedi\'i amlygu</string> + <string name="action_access_scheduled_posts">Negeseuon wedi\'u hamserlennu</string> + <string name="action_schedule_post">Amserlennu Neges</string> + <string name="action_mentions">Crybwylliadau</string> + <string name="dialog_mute_warning">Tewi @%1$s?</string> + <string name="dialog_mute_hide_notifications">Cuddio hysbysiadau</string> + <string name="pref_title_notification_filter_sign_ups">mae rhywun wedi cofrestru</string> + <string name="pref_title_timeline_filters">Hidlyddion</string> + <string name="dialog_block_warning">Rhwystro @%1$s?</string> + <string name="notification_subscription_name">Negeseuon newydd</string> + <string name="notification_update_name">Golygiadau\'r neges</string> + <string name="notification_poll_name">Polau</string> + <string name="notification_sign_up_name">Cofrestriadau</string> + <string name="pref_title_public_filter_keywords">Ffrydiau cyhoeddus</string> + <string name="filter_addition_title">Ychwanegu hidlydd</string> + <string name="filter_edit_title">Golygu hidlydd</string> + <string name="filter_dialog_update_button">Diweddaru</string> + <string name="pref_title_notification_filter_updates">golygwyd neges rydych chi wedi rhyngweithio ag ef</string> + <string name="notification_update_format">Golygodd %1$s ei neges</string> + <string name="filter_apply">Gosod</string> + <string name="pref_title_notification_filter_follow_requests">dilyn y ceisiwyd amdani/o</string> + <string name="notifications_clear">Dileu</string> + <string name="pref_title_notification_filter_poll">polau wedi dod i ben</string> + <string name="pref_title_notification_filter_subscriptions">mae rhywun dw i\'n tanysgrifio iddi/o wedi cyhoeddi neges newydd</string> + <string name="filter_dialog_whole_word">Gair cyfan</string> + <string name="filter_add_description">Ymadrodd i\'w hidlo</string> + <string name="action_open_as">Agor fel %1$s</string> + <string name="action_reset_schedule">Ailosod</string> + <string name="title_bookmarks">Tudalnodau</string> + <string name="notification_follow_request_format">Ceisiodd %1$s i\'ch dilyn chi</string> + <string name="title_login">Mewngofnodi</string> + <string name="notification_sign_up_format">Cofrestrodd %1$s</string> + <string name="action_unbookmark">Tynnu\'r tudalnod</string> + <string name="action_unmute_desc">Dad-dewi %1$s</string> + <string name="title_migration_relogin">Mewngofnodwch eto i gael hysbysiadau gwthio</string> + <string name="title_announcements">Cyhoeddiadau</string> + <string name="error_could_not_load_login_page">Methu llwytho\'r dudalen mewngofnodi.</string> + <string name="title_scheduled_posts">Negeseuon wedi\'u hamserlennu</string> + <string name="error_loading_account_details">Wedi methu llwytho manylion y cyfrif</string> + <string name="pref_title_timelines">Ffrydiau</string> + <string name="title_favourited_by">Hoffwyd gan</string> + <string name="error_image_edit_failed">Methu golygu\'r ddelwedd.</string> + <string name="description_post_favourited">Hoffwyd</string> + <string name="saving_draft">Yn cadw drafft…</string> + <string name="action_hashtags">Hashnodau</string> + <string name="action_dismiss">Diystyru</string> + <string name="action_details">Manylion</string> + <string name="title_mentions_dialog">Crybwylliadau</string> + <string name="action_open_media_n">Agor cyfryngau #%1$d</string> + <string name="action_share_as">Rhannu fel …</string> + <string name="downloading_media">Yn lawrlwytho cyfryngau</string> + <string name="download_media">Lawrlwytho cyfryngau</string> + <string name="dialog_redraft_post_warning">Dileu ac ailddrafftio\'r neges hon\?</string> + <string name="dialog_delete_conversation_warning">Dileu\'r sgwrs hon\?</string> + <string name="pref_title_language">Iaith</string> + <string name="pref_title_bot_overlay">Dangos dangosydd botiau</string> + <string name="pref_title_gradient_for_media">Dangos graddiannau lliwgar ar gyfer cyfryngau cudd</string> + <string name="pref_failed_to_sync">Methu cydweddu dewisiadau</string> + <string name="pref_main_nav_position_option_top">Brig</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="title_hashtags_dialog">Hashnodau</string> + <string name="pref_main_nav_position_option_bottom">Gwaelod</string> + <string name="action_open_faved_by">Dangos ffefrynnau</string> + <string name="report_description_1">Bydd yr adroddiad yn cael ei anfon at reolwr eich gweinydd. Gallwch chi esbonio pan rydych chi\'n adrodd ar y cyfrif hwn isod:</string> + <string name="warning_scheduling_interval">Mae gan Mastodon egwyl amserlennu o o leiaf 5 munud.</string> + <string name="follow_requests_info">Er nad ydych eich cyfrif wedi\'i gloi, roedd staff y %1$s yn meddwl efallai yr hoffwch chi adolygu\'r ceisiadau i\'ch dilyn o\'r cyfrifon hyn â llaw.</string> + <string name="delete_scheduled_post_warning">Dileu\'r neges wedi\'u hamserlennu hon\?</string> + <string name="description_post_media_no_description_placeholder">Dim disgrifiad</string> + <string name="hint_list_name">Enw\'r rhestr</string> + <string name="edit_hashtag_hint">Hashnod heb #</string> + <string name="report_remote_instance">Anfon ymlaen at %1$s</string> + <string name="report_description_remote_instance">Daw\'r cyfrif o weinydd arall. Ydych chi am anfon copi dienw o\'r adroddiad i\'r gweinydd hwnnw hefyd\?</string> + <string name="duration_1_hour">Awr</string> + <string name="duration_6_hours">6 awr</string> + <string name="duration_1_day">Diwrnod</string> + <string name="unpin_action">Dadbinio</string> + <string name="pref_show_self_username_never">Byth</string> + <string name="pref_show_self_username_always">Bob tro</string> + <string name="post_media_attachments">Atodiadau</string> + <string name="action_create_list">Creu rhestr</string> + <string name="action_rename_list">Diweddaru\'r rhestr</string> + <string name="hint_search_people_list">Chwilio am bobl rydych chi\'n eu dilyn</string> + <string name="profile_badge_bot_text">Bot</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="action_edit_image">Golygu llun</string> + <string name="title_reblogged_by">Hybwyd gan</string> + <string name="label_duration">Hyd</string> + <string name="duration_5_min">5 munud</string> + <string name="duration_30_min">Hanner awr</string> + <string name="duration_3_days">Tridiau</string> + <string name="duration_7_days">Wythnos</string> + <string name="duration_14_days">Pythefnos</string> + <string name="duration_30_days">30 diwrnod</string> + <string name="duration_60_days">60 diwrnod</string> + <string name="duration_90_days">90 diwrnod</string> + <string name="duration_180_days">180 diwrnod</string> + <string name="duration_365_days">Blwyddyn</string> + <string name="add_poll_choice">Ychwanegu dewis</string> + <string name="pin_action">Pinio</string> + <string name="conversation_more_recipients">%1$s, %2$s a %3$d eraill</string> + <string name="poll_info_time_absolute">yn dod i ben am %1$s</string> + <string name="duration_no_change">(Dim newid)</string> + <string name="poll_allow_multiple_choices">Amlddewis</string> + <string name="poll_new_choice_hint">Dewis %1$d</string> + <string name="no_announcements">Does dim cyhoeddiadau.</string> + <string name="account_note_hint">Eich nodyn preifat ynghylch y cyfrif hwn</string> + <string name="account_note_saved">Cadwyd!</string> + <string name="action_subscribe_account">Tanysgrifio</string> + <string name="action_unsubscribe_account">Dad-danysgrifo</string> + <string name="dialog_delete_list_warning">Ydych chi wir eisiau dileu rhestr %1$s?</string> + <string name="no_drafts">Does gennych chi ddim drafftiau.</string> + <string name="pref_title_show_cards_in_timelines">Dangos rhagolygon dolen mewn ffrydiau</string> + <string name="pref_title_wellbeing_mode">Llesiant</string> + <string name="wellbeing_hide_stats_posts">Cuddio ystadegau meintiol negeseuon</string> + <string name="wellbeing_hide_stats_profile">Cuddio ystadegau meintiol proffiliau</string> + <string name="draft_deleted">Drafft wedi\'i ddileu</string> + <string name="drafts_post_reply_removed">Mae\'r neges y drafftioch chi ymateb iddi wedi cael ei dileu</string> + <string name="description_post_language">Iaith y neges</string> + <string name="hashtags">Hashnodau</string> + <string name="poll_vote">Pleidleisio</string> + <string name="poll_ended_voted">Mae pôl rydych chi wedi pleidleisio ynddo wedi dod i ben</string> + <string name="poll_ended_created">Mae pôl a grëwyd gennych wedi dod i ben</string> + <string name="button_continue">Parhau</string> + <string name="button_back">Nôl</string> + <string name="hint_additional_info">Sylwadau ychwanegol</string> + <string name="app_theme_system">Defnyddio arddull y system</string> + <string name="pref_main_nav_position">Prif lleoliad y panel llywio</string> + <string name="pref_title_animate_gif_avatars">Animeiddio lluniau proffil GIF</string> + <string name="pref_default_media_sensitivity">Nodi cyfryngau yn sensitif bob tro (bydd yn cael ei gydweddu gyda\'ch gweinydd)</string> + <string name="about_powered_by_tusky">Pwerir gan Tusky</string> + <string name="title_accounts">Cyfrifon</string> + <string name="create_poll_title">Pôl</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="action_delete_list">Dileu\'r rhestr</string> + <string name="add_hashtag_title">Ychwanegu hashnod</string> + <string name="status_count_one_plus">1+</string> + <string name="post_media_audio">Sain</string> + <string name="compose_shortcut_long_label">Ysgrifennu neges</string> + <string name="drafts_post_failed_to_send">Methodd anfon y neges hon!</string> + <string name="tusky_compose_post_quicksetting_label">Ysgrifennu Neges</string> + <string name="tips_push_notification_migration">Mewngofnodwch i\'ch gyfrifon eto er mwyn galluogi hysbysiadau i\'ch ffôn.</string> + <string name="dialog_push_notification_migration">Er mwyn derbyn hysbysiadau i\'ch ffôn drwy UnifiedPush, mae angen caniatâd ar Tusky i danysgrifio i hysbysiadau ar eich gweinydd Mastodon. Bydd rhaid i chi mewngofnodi eto i newid y sgôp OAuth sy\'n cael ei roi i Tusky. Bydd defnyddio\'r opsiwn ail-fewngofnodi yma neu yn Newisiadau cyfrif yn cadw eich holl ddrafftiau lleol a\'ch cof dros dro.</string> + <string name="mute_domain_warning">Ydych chi\'n siŵr eich bod am rwystro %1$s gyfan? Fyddwch chi ddim yn gweld dim cynnwys o\'r parth hwnnw mewn unrhyw ffrwd gyhoeddus na chwaith yn eich hysbysiadau. Bydd eich dilynwyr o\'r parth hwnnw yn cael eu dileu.</string> + <string name="duration_indefinite">Dim diwedd</string> + <plurals name="favs"> + <item quantity="zero"><b>%1$s</b> Ffefryn</item> + <item quantity="one"><b>%1$s</b> Ffefryn</item> + <item quantity="two"><b>%1$s</b> Ffefryn</item> + <item quantity="few"><b>%1$s</b> Ffefryn</item> + <item quantity="many"><b>%1$s</b> Ffefryn</item> + <item quantity="other"><b>%1$s</b> Ffefryn</item> + </plurals> + <string name="description_visibility_direct">Uniongyrchol</string> + <string name="action_add_or_remove_from_list">Ychwanegu at neu dynnu oddi ar restr</string> + <string name="failed_to_add_to_list">Wedi methu ag ychwanegu\'r cyfrif at y rhestr</string> + <string name="failed_to_remove_from_list">Wedi methu tynnu\'r cyfrif o\'r rhestr</string> + <string name="instance_rule_info">Drwy fewngofnodi rydych chi\'n cytuno i reolau %1$s.</string> + <string name="compose_save_draft_loses_media">Cadw drafft\? (Bydd atodiadau\'n cael eu llwytho i fyny eto pan fyddwch chi\'n adfer y drafft.)</string> + <string name="description_post_reblogged">Ailflogiwyd</string> + <string name="dialog_push_notification_migration_other_accounts">Rydych chi wedi mewngofnodi i\'ch cyfrif cyfredol eto i roi caniatâd tanysgrifio gwthio i Tusky. Fodd bynnag, mae gennych chi gyfrifon eraill o hyd nad ydyn nhw wedi\'u mudo fel hyn. Newidiwch atyn nhw a mewngofnodwch eto fesul un er mwyn galluogi cefnogaeth hysbysiadau UnifiedPush.</string> + <string name="poll_info_format"> \u0020<!-- 15 votes • 1 hour left --> \u0020%1$s • %2$s</string> + <string name="hint_media_description_missing">Dylai fod gan y cyfryngau ddisgrifiad.</string> + <string name="description_post_cw">Rhybudd cynnwys: %1$s</string> + <string name="pref_title_confirm_reblogs">Dangos cadarnhad cyn hybu</string> + <string name="review_notifications">Adolygu hysbysiadau</string> + <string name="notification_report_format">Adroddiad newydd ar %1$s</string> + <string name="notification_header_report_format">Adroddodd %1$s %2$s</string> + <string name="notification_summary_report_format">%1$s · %2$d o negeseuon wedi\'u hatodi</string> + <string name="pref_title_notification_filter_reports">mae yna adroddiad newydd</string> + <string name="report_category_violation">Toriad rheol</string> + <string name="report_category_spam">Sbam</string> + <string name="failed_to_pin">Wedi methu Pinio</string> + <string name="failed_to_unpin">Wedi methu Dadbinio</string> + <string name="pref_title_http_proxy_port_message">Dylai\'r porth fod rhwng %1$d a %2$d</string> + <string name="error_rename_list">Methu diweddaru\'r rhestr</string> + <string name="pref_default_post_language">Iaith bostio ragosodedig (bydd yn cael ei gydweddu gyda\'ch gweinydd)</string> + <string name="description_post_bookmarked">Tudalnodiwyd</string> + <string name="select_list_title">Dewiswch restr</string> + <string name="button_done">Wedi gorffen</string> + <string name="report_sent_success">Wedi adrodd ar @%1$s yn llwyddiannus</string> + <string name="limit_notifications">Cyfyngu ar hysbysiadau ffrwd</string> + <string name="post_lookup_error_format">Gwall wrth chwilio am y neges %1$s</string> + <string name="pref_title_show_self_username">Dangos enw defnyddiwr mewn bariau offer</string> + <string name="pref_title_confirm_favourites">Dangos cadarnhad cyn hoffi</string> + <string name="pref_title_hide_top_toolbar">Cuddio teitl y bar offer uchaf</string> + <string name="instance_rule_title">Rheolau %1$s</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="error_following_hashtags_unsupported">Nid yw\'r gweinydd hwn yn cefnogi\'r hashnodau canlynol.</string> + <string name="poll_info_closed">wedi cau</string> + <string name="failed_fetch_posts">Wedi methu nôl negeseuon</string> + <string name="error_muting_hashtag_format">Gwall wrth dewi #%1$s</string> + <string name="error_unmuting_hashtag_format">Gwall wrth ddad-dewi #%1$s</string> + <string name="title_followed_hashtags">Hashnodau wedi\'u dilyn</string> + <string name="notification_report_name">Adroddiadau</string> + <string name="label_remote_account">Gall y wybodaeth isod adlewyrchu proffil y defnyddiwr yn anghyflawn. Pwyswch i agor y proffil llawn mewn porwr.</string> + <plurals name="reblogs"> + <item quantity="zero"><b>%1$s</b> Hybiau</item> + <item quantity="one"><b>%1$s</b> Hwb</item> + <item quantity="two"><b>%1$s</b> Hwb</item> + <item quantity="few"><b>%1$s</b> Hwb</item> + <item quantity="many"><b>%1$s</b> Hwb</item> + <item quantity="other"><b>%1$s</b> Hwb</item> + </plurals> + <string name="conversation_1_recipients">%1$s</string> + <string name="description_post_edited">Golygwyd</string> + <string name="pref_show_self_username_disambiguate">Pan fydd cyfrifon lluosog wedi\'u mewngofnodi</string> + <string name="drafts_failed_loading_reply">Wedi methu llwytho gwybodaeth Ateb</string> + <string name="pref_title_alway_open_spoiler">Agor negeseuon wedi\'u marcio â rhybudd cynnwys bob tro</string> + <string name="profile_metadata_label_label">Label</string> + <string name="post_edited">Golygwyd %1$s</string> + <string name="confirmation_hashtag_unfollowed">#%1$s wedi\'i ddad-ddilyn</string> + <string name="pref_title_animate_custom_emojis">Animeiddio emojis cyfaddas</string> + <string name="notification_subscription_description">Hysbysiadau pan fydd rhywun rydych chi wedi tanysgrifio iddyn nhw\'n cyhoeddi neges newydd</string> + <string name="notification_sign_up_description">Hysbysiadau am ddefnyddwyr newydd</string> + <string name="notification_report_description">Hysbysiadau am adroddiadau cymedroli</string> + <string name="filter_dialog_whole_word_description">Os yw\'r allweddair neu\'r ymadrodd yn alffaniwmerig yn unig, mi fydd ond yn cael ei osod os yw\'n cyfateb â\'r gair cyfan</string> + <string name="error_create_list">Methu creu rhestr</string> + <string name="set_focus_description">Tapiwch neu lusgwch y cylch i ddewis y canolbwynt a fydd bob amser yn weladwy mewn cryno-luniau.</string> + <string name="description_poll">Pôl gyda dewisiadau: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="list">Rhestr</string> + <string name="action_set_focus">Gosod pwynt ffocws</string> + <string name="status_created_at_now">nawr</string> + <string name="error_delete_list">Methu dileu\'r rhestr</string> + <string name="action_add_to_list">Ychwanegu cyfrif at y rhestr</string> + <string name="action_remove_from_list">Tynnu cyfrif o\'r rhestr</string> + <string name="action_add_reaction">ychwanegu ymateb</string> + <string name="description_post_media">Cyfryngau: %1$s</string> + <string name="error_status_source_load">Wedi methu llwytho ffynhonnell y statws o\'r gweinydd.</string> + <string name="pref_title_enable_swipe_for_tabs">Galluogi ystum llusgo i newid rhwng tabiau</string> + <plurals name="poll_info_votes"> + <item quantity="zero">%1$s pleidlais</item> + <item quantity="one">%1$s pleidlais</item> + <item quantity="two">%1$s bleidlais</item> + <item quantity="few">%1$s pleidlais</item> + <item quantity="many">%1$s pleidlais</item> + <item quantity="other">%1$s phleidlais</item> + </plurals> + <plurals name="poll_info_people"> + <item quantity="zero">%1$s person</item> + <item quantity="one">%1$s person</item> + <item quantity="two">%1$s berson</item> + <item quantity="few">%1$s person</item> + <item quantity="many">%1$s person</item> + <item quantity="other">%1$s person</item> + </plurals> + <string name="no_lists">Nid oes gennych unrhyw restrau.</string> + <string name="wellbeing_mode_notice">Bydd rhywfaint o wybodaeth a all effeithio ar eich lles meddyliol yn cael ei chuddio. Mae hyn yn cynnwys: +\n +\n - Hysbysiadau hoffi/hybu/dilyn +\n - Cyfrif ffefrynnau/hybiau ar negeseuon +\n - Ystadegau dilynwr/negeseuon ar broffiliau +\n +\n Ni fydd hysbysiadau gwthio yn cael eu heffeithio, ond gallwch chi adolygu eich dewisiadau hysbysu â llaw.</string> + <string name="report_category_other">Arall</string> + <string name="action_unfollow_hashtag_format">Dad-ddilyn #%1$s?</string> + <string name="notification_update_description">Hysbysiadau pan fydd negeseuon rydych chi wedi rhyngweithio â nhw yn cael eu golygu</string> + <string name="failed_search">Wedi methu â chwilio</string> + <string name="failed_report">Wedi methu ag adrodd</string> + <string name="compose_preview_image_description">Gweithredoedd ar gyfer delwedd %1$s</string> + <string name="action_open_reblogger">Agor awdur hybu</string> + <string name="notification_follow_request_description">Hysbysiadau am geisiadau i\'ch dilyn</string> + <string name="caption_notoemoji">Set emoji cyfredol Google</string> + <plurals name="poll_timespan_seconds"> + <item quantity="zero">%1$d eiliadau ar ôl</item> + <item quantity="one">%1$d eiliad ar ôl</item> + <item quantity="two">%1$d eiliad ar ôl</item> + <item quantity="few">%1$d eiliad ar ôl</item> + <item quantity="many">%1$d eiliad ar ôl</item> + <item quantity="other">%1$d eiliad ar ôl</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="zero">%1$d oriau ar ôl</item> + <item quantity="one">%1$d awr ar ôl</item> + <item quantity="two">%1$d awr ar ôl</item> + <item quantity="few">%1$d awr ar ôl</item> + <item quantity="many">%1$d awr ar ôl</item> + <item quantity="other">%1$d awr ar ôl</item> + </plurals> + <plurals name="poll_timespan_days"> + <item quantity="zero">%1$d diwrnodau ar ôl</item> + <item quantity="one">%1$d diwrnod ar ôl</item> + <item quantity="two">%1$d ddiwrnod ar ôl</item> + <item quantity="few">%1$d diwrnod ar ôl</item> + <item quantity="many">%1$d diwrnod ar ôl</item> + <item quantity="other">%1$d niwrnod ar ôl</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="zero">%1$d munudau ar ôl</item> + <item quantity="one">%1$d munud ar ôl</item> + <item quantity="two">%1$d funud ar ôl</item> + <item quantity="few">%1$d munud ar ôl</item> + <item quantity="many">%1$d munud ar ôl</item> + <item quantity="other">%1$d munud ar ôl</item> + </plurals> + <plurals name="error_upload_max_media_reached"> + <item quantity="zero">Nid oes modd i ti lanlwytho mwy na %1$d atodiadau cyfryngau.</item> + <item quantity="one">Nid oes modd i ti lanlwytho mwy nag %1$d atodiad cyfryngau.</item> + <item quantity="two">Nid oes modd i ti lanlwytho mwy na %1$d atodiad cyfryngau.</item> + <item quantity="few">Nid oes modd i ti lanlwytho mwy na %1$d atodiad cyfryngau.</item> + <item quantity="many">Nid oes modd i ti lanlwytho mwy na %1$d atodiad cyfryngau.</item> + <item quantity="other">Nid oes modd i ti lanlwytho mwy na %1$d atodiad cyfryngau.</item> + </plurals> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="zero">Disgrifiwch y cynnwys ar gyfer pobl â nam ar eu golwg (terfyn nodau o %1$d)</item> + <item quantity="one">Disgrifiwch y cynnwys ar gyfer pobl â nam ar eu golwg (terfyn nodau o %1$d)</item> + <item quantity="two">Disgrifiwch y cynnwys ar gyfer pobl â nam ar eu golwg (terfyn nodau o %1$d)</item> + <item quantity="few">Disgrifiwch y cynnwys ar gyfer pobl â nam ar eu golwg (terfyn nodau o %1$d)</item> + <item quantity="many">Disgrifiwch y cynnwys ar gyfer pobl â nam ar eu golwg (terfyn nodau o %1$d)</item> + <item quantity="other">Disgrifiwch y cynnwys ar gyfer pobl â nam ar eu golwg (terfyn nodau o %1$d)</item> + </plurals> + <string name="action_continue_edit">Parhau i olygu</string> + <string name="status_edit_info">Golygwyd: %1$s</string> + <string name="status_created_info">Crëwyd: %1$s</string> + <string name="title_edits">Golygiadau</string> + <string name="post_media_alt">DISGRIFIAD</string> + <string name="action_discard">Hepgor newidiadau</string> + <string name="compose_unsaved_changes">Mae gennych chi newidiadau heb eu cadw.</string> + <string name="mute_notifications_switch">Tewi hysbysiadau</string> + <string name="a11y_label_loading_thread">Yn llwytho edefyn</string> + <string name="action_share_account_link">Rhannu ddolen i gyfrif</string> + <string name="action_share_account_username">Rhannu enw defnyddiwr y cyfrif</string> + <string name="send_account_link_to">Rhannu URL cyfrif i…</string> + <string name="send_account_username_to">Rhannu enw denyddiwr cyfrif i…</string> + <string name="account_username_copied">Enw defnyddiwr wedi\'i gopïo</string> + <string name="pref_reading_order_oldest_first">Hynaf yn gyntaf</string> + <string name="pref_reading_order_newest_first">Diweddaraf yn gyntaf</string> + <string name="pref_title_reading_order">Trefn darllen</string> + <string name="pref_summary_http_proxy_disabled">Wedi\'i analluogi</string> + <string name="pref_summary_http_proxy_missing"><heb ei osod></string> + <string name="pref_summary_http_proxy_invalid"><annilys></string> + <string name="action_browser_login">Mewngofnodi â phorwr</string> + <string name="description_login">Yn gweithio yn y rhan mwyaf o achosion. Nid oes unrhyw ddata yn cael ei ollwng i apiau eraill.</string> + <string name="description_browser_login">Gall gefnogi dulliau dilysu ychwanegol, ond mae angen porwr a gefnogir.</string> + <string name="action_post_failed_detail">Methodd eich neges â llwytho i fyny a chafodd ei chadw i\'ch drafftiau. +\n +\nNaill ai nid oedd modd cysylltu â\'r gweinydd, neu fe wrthododd y neges.</string> + <string name="action_post_failed">Wedi methu llwytho i fyny</string> + <string name="action_post_failed_show_drafts">Dangos drafftiau</string> + <string name="action_post_failed_do_nothing">Diystyru</string> + <string name="action_post_failed_detail_plural">Methodd eich negeseuon â llwytho i fyny a chafodd ei chadw i\'ch drafftiau. +\n +\nNaill ai nid oedd modd cysylltu â\'r gweinydd, neu fe wrthododd y negeseuon.</string> + <string name="title_public_trending_hashtags">Hashnodau tueddiadol</string> + <string name="accessibility_talking_about_tag">Mae %1$d o bobl yn siarad am hashnod %2$s</string> + <string name="total_usage">Defnydd cyfan</string> + <string name="total_accounts">Cyfrifon cyfan</string> + <string name="dialog_follow_hashtag_title">Dilyn hashnod</string> + <string name="dialog_follow_hashtag_hint">#hashnod</string> + <string name="action_refresh">Adnewyddu</string> + <string name="notification_unknown_name">Anhysbys</string> + <string name="status_filtered_show_anyway">Dangos beth bynnag</string> + <string name="status_filter_placeholder_label_format">Hidlwyd: %1$s</string> + <string name="pref_title_account_filter_keywords">Proffiliau</string> + <string name="socket_timeout_exception">Cymrodd hi\'n rhy hir i gysylltu â\'ch weinydd</string> + <string name="ui_error_unknown">rheswm anhysbys</string> + <string name="ui_error_bookmark">Methodd tudalnodi\'r neges: %1$s</string> + <string name="ui_error_clear_notifications">Methodd clirio hysbysiadau: %1$s</string> + <string name="ui_error_favourite">Methodd hoffi\'r neges: %1$s</string> + <string name="ui_error_reblog">Methodd hybu\'r neges: %1$s</string> + <string name="ui_error_vote">Methodd y pleidleisio: %1$s</string> + <string name="ui_error_accept_follow_request">Methodd derbyn cais i ddilyn: %1$s</string> + <string name="ui_error_reject_follow_request">Methodd gwrthod cais i ddilyn: %1$s</string> + <string name="ui_success_accepted_follow_request">Cais i ddilyn wedi\'i dderbyn</string> + <string name="ui_success_rejected_follow_request">Rhwystrwyd cais i ddilyn</string> + <string name="hint_filter_title">Fy hidl</string> + <string name="label_filter_title">Teitl</string> + <string name="filter_action_warn">Rhybudd</string> + <string name="filter_action_hide">Cuddio</string> + <string name="filter_description_warn">Cuddio gyda rhybudd</string> + <string name="filter_description_hide">Cuddio\'n llwyr</string> + <string name="label_filter_action">Gweithredoedd hidlo</string> + <string name="label_filter_context">Hidlo cyd-destynau</string> + <string name="label_filter_keywords">Allweddeiriau neu ymadroddion i\'w hidlo</string> + <string name="action_add">Ychwanegu</string> + <string name="filter_keyword_display_format">%1$s (gair cyfan)</string> + <string name="filter_keyword_addition_title">Ychwanegu allweddair</string> + <string name="filter_edit_keyword_title">Golygu allweddair</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="post_media_image">Delwedd</string> + <string name="select_list_manage">Rheoli rhestrau</string> + <string name="help_empty_home">Dyma\'ch <b>ffrwd cartref</b>. Mae\'n dangos negeseuon diweddar y cyfrifon rydych chi\'n eu dilyn. \u0020 +\n \u0020 +\nEr mwyn archwilio cyfrifon gallwch chi dod o hyd iddyn o fewn un o\'r ffrydiau eraill. Er enghraifft, ffrwd eich gweinydd [iconics gmd_group]. Neu gallwch chi eu chwilio yn ôl eu henw [iconics gmd_search]; er enghraifft, chwiliwch am Tusky i ddod o hyd ein cyfrif Mastodon.</string> + <string name="pref_title_show_stat_inline">Dangos ystadegau negeseuon yn y ffrwd</string> + <string name="pref_ui_text_size">Maint testun rhyngwyneb</string> + <string name="notification_listenable_worker_name">Gweithgaredd cefndirol</string> + <string name="notification_listenable_worker_description">Hysbysiadau pan fydd Tusky\'n gweithio\'n y cefndir</string> + <string name="notification_notification_worker">Yn estyn hysbysiadau…</string> + <string name="notification_prune_cache">Cynnal a chadw\'r cof dros dro…</string> + <string name="error_missing_edits">Mae eich gweinydd yn gwybod y cafodd y neges hon ei golygu, ond nid oes ganddo gopi o\'r golygiadau, felly nid oes modd eu dangos i chi. +\n +\nDyma <a href="https://github.com/mastodon/mastodon/issues/25398">broblem Mastodon #25398</a>.</string> + <string name="load_newest_notifications">Llwytho hysbysiadau diweddaraf</string> + <string name="compose_delete_draft">Dileu\'r drafft\?</string> + <string name="error_media_upload_sending_fmt">Methodd llwytho i fyny: %1$s</string> + <string name="about_device_info_title">Eich dyfais</string> + <string name="about_device_info">%1$s %2$s +\nFersiwn Android: %3$s +\nFersiwn SDK: %4$d</string> + <string name="about_account_info_title">Eich cyfrif</string> + <string name="about_account_info">\@%1$s@%2$s +\nFersiwn: %3$s</string> + <string name="about_copy">Copïo fersiwn a gwybodaeth dyfais</string> + <string name="about_copied">Copïwyd fersiwn a gwybodaeth dyfais</string> + <string name="list_exclusive_label">Cuddio o\'r ffrwd cartref</string> + <string name="error_media_playback">Methodd chwarae: %1$s</string> + <string name="dialog_delete_filter_positive_action">Dileu</string> + <string name="dialog_delete_filter_text">Dileu\'r hidlydd \'%1$s\'\?</string> + <string name="dialog_save_profile_changes_message">Hoffech chi gadw\'r newidiadau i\'ch proffil\?</string> + <string name="label_image">Delwedd</string> + <string name="app_theme_system_black">Defnyddio Cynllun y System (du)</string> + <string name="help_empty_conversations">Dyma\'ch <b>negeseuon preifat</b>; weithiau\'n cael eu galw\'n sgyrsiau neu negeseuon uniongyrchol. \u0020 +\n \u0020 +\nMae negeseuon preifat yn cael eu creu drwy osod y gwelededd [iconics gmd_public] neges i [iconics gmd_mail] <i>Uniongyrchol</i> a chyfeirio at un neu ragor o ddefnyddwyr yn y testun. \u0020 +\n \u0020 +\nEr enghraifft gallwch chi dechrau ar y proffil golwg cyfrif a phwyso\'r botwm creu [iconics gmd_edit] a newid y gwelededd. \u0020</string> + <string name="unmuting_hashtag_success_format">Yn dad-dewi hashnod #%1$s</string> + <string name="muting_hashtag_success_format">Yn tewi hashnod #%1$s fel rhybudd</string> + <string name="unfollowing_hashtag_success_format">Ddim yn dilyn hashnod #%1$s bellach</string> + <string name="title_public_trending_statuses">Negeseuon tueddiadol</string> + <string name="action_view_filter">Gweld hidl</string> + <string name="following_hashtag_success_format">Bellach yn dilyn yr hashnod #%1$s</string> + <string name="error_unblocking_domain">Wedi methu dad-dewi %1$s: %2$s</string> + <string name="error_blocking_domain">Wedi methu tewi %1$s: %2$s</string> + <string name="help_empty_lists">Dyma\'ch <b>golwg rhestri</b>. Gallwch chi diffinio nifer o restri preifat ac yn ychwanegu cyfrifiadau atyn. \u0020 +\n \u0020 +\n \u0020SYLWCH y gallwch chi ychwanegu dim ond cyfrifiadau eich bod chi\'n dilyn i\'ch rhestri. \u0020 +\n \u0020 +\n \u0020Gall y rhestri hyn yn cael eu defnyddio fel tab yn Newisiadau cyfrif [iconics gmd_account_circle] [iconics gmd_navigate_next] Tabiau. \u0020</string> + <string name="list_reply_policy_list">Aelodau\'r rhestr</string> + <string name="list_reply_policy_followed">Unrhyw ddefnyddiwr a ddilynir</string> + <string name="list_reply_policy_label">Dangos ymatebion i</string> + <string name="list_reply_policy_none">Neb</string> + <string name="pref_title_show_self_boosts">Dangos hunan-hybiau</string> + <string name="pref_title_show_self_boosts_description">Rhywun yn hybu ei neges ei hunan</string> + <string name="pref_title_per_timeline_preferences">Dewisiadau i bob ffrwd</string> + <string name="pref_title_show_notifications_filter">Dangos hidlydd Hysbysiadau</string> + <string name="reply_sending_long">Mae eich ateb yn cael ei anfon.</string> + <string name="reply_sending">Yn anfon…</string> + <string name="action_translate">Cyfieithu</string> + <string name="action_show_original">Dangos y gwreiddiol</string> + <string name="label_translating">Yn cyfieithu…</string> + <string name="label_translated">Wedi\'i chyfieithu o %1$s gyda %2$s</string> + <string name="ui_error_translate">Nid oedd modd cyfieithu: %1$s</string> + <string name="report_category_legal">Cyfreithiol</string> + <string name="unknown_notification_type">Math anhysbys o hysbysiad</string> + <string name="dialog_follow_warning">Dilyn y cyfrif hwn?</string> + <string name="pref_title_confirm_follows">Dangos cadarnhad cyn dilyn</string> + <string name="url_copied">Dolen wedi\'i chopïo</string> + <string name="confirmation_hashtag_copied">\'#%1$s\' wedi\'i gopïo</string> + <string name="pref_default_reply_privacy">Preifatrwydd rhagosodedig ymatebion (ni fydd yn cael ei gydweddu gyda\'ch gweinydd)</string> + <string name="error_deleting_filter">Bu gwall wrth ddileu hidlydd \'%1$s\'</string> + <string name="error_saving_filter">Bu gwall wrth gadw hidlydd \'%1$s\'</string> + <string name="action_follow_hashtag">Dilyn hashnod newydd</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml new file mode 100644 index 0000000..1573df1 --- /dev/null +++ b/app/src/main/res/values-de/strings.xml @@ -0,0 +1,719 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Ein Fehler ist aufgetreten.</string> + <string name="error_network">Ein Netzwerkfehler ist aufgetreten. Bitte überprüfe deine Internetverbindung und versuche es erneut.</string> + <string name="error_empty">Das darf nicht leer sein.</string> + <string name="error_invalid_domain">Ungültige Domain angegeben</string> + <string name="error_failed_app_registration">Authentifizieren mit dieser Instanz fehlgeschlagen. Falls das Problem weiter besteht, versuche dich über den Browser anzumelden.</string> + <string name="error_no_web_browser_found">Kein Webbrowser gefunden.</string> + <string name="error_authorization_unknown">Ein unbekannter Fehler ist bei der Autorisierung aufgetreten. Falls das Problem weiter besteht, versuche dich über den Browser anzumelden.</string> + <string name="error_authorization_denied">Autorisierung wurde abgelehnt. Wenn du dir sicher bist, dass du die korrekten Anmeldedaten eingegeben hast, versuche dich über den Browser anzumelden.</string> + <string name="error_retrieving_oauth_token">Es konnte kein Anmelde-Token abgerufen werden. Falls das Problem weiter besteht, versuche dich über den Browser anzumelden.</string> + <string name="error_compose_character_limit">Der Beitrag ist zu lang!</string> + <string name="error_media_upload_type">Dieser Dateityp kann nicht hochgeladen werden.</string> + <string name="error_media_upload_opening">Die Datei konnte nicht geöffnet werden.</string> + <string name="error_media_upload_permission">Berechtigung für Zugriff auf Mediendateien benötigt.</string> + <string name="error_media_download_permission">Berechtigung fürs Speichern von Mediendateien benötigt.</string> + <string name="error_media_upload_image_or_video">Bilder und Videos können nicht an den gleichen Beitrag angehängt werden.</string> + <string name="error_media_upload_sending">Das Hochladen ist fehlgeschlagen.</string> + <string name="error_sender_account_gone">Fehler beim Senden des Beitrags.</string> + <string name="title_home">Startseite</string> + <string name="title_notifications">Benachrichtigungen</string> + <string name="title_public_local">Lokal</string> + <string name="title_public_federated">Föderiert</string> + <string name="title_direct_messages">Direktnachrichten</string> + <string name="title_tab_preferences">Tabs</string> + <string name="title_view_thread">Thread</string> + <string name="title_posts">Beiträge</string> + <string name="title_posts_with_replies">Mit Antworten</string> + <string name="title_posts_pinned">Angeheftet</string> + <string name="title_follows">Folge ich</string> + <string name="title_followers">Follower</string> + <string name="title_favourites">Favoriten</string> + <string name="title_mutes">Stummgeschaltete Profile</string> + <string name="title_blocks">Blockierte Profile</string> + <string name="title_follow_requests">Follower-Anfragen</string> + <string name="title_edit_profile">Dein Profil bearbeiten</string> + <string name="title_drafts">Entwürfe</string> + <string name="title_licenses">Lizenzen</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s teilte</string> + <string name="post_sensitive_media_title">Inhaltswarnung</string> + <string name="post_media_hidden_title">Mediendateien ausgeblendet</string> + <string name="post_media_alt">ALT</string> + <string name="post_sensitive_media_directions">Zum Anzeigen tippen</string> + <string name="post_content_warning_show_more">Mehr anzeigen</string> + <string name="post_content_warning_show_less">Weniger anzeigen</string> + <string name="post_content_show_more">Ausklappen</string> + <string name="post_content_show_less">Einklappen</string> + <string name="message_empty">Hier ist nichts.</string> + <string name="footer_empty">Noch keine Beiträge. Ziehe zum Aktualisieren nach unten!</string> + <string name="notification_reblog_format">%1$s teilte deinen Beitrag</string> + <string name="notification_favourite_format">%1$s favorisierte deinen Beitrag</string> + <string name="notification_follow_format">%1$s folgt dir</string> + <string name="report_username_format">\@%1$s melden</string> + <string name="report_comment_hint">Ergänzende Hinweise\?</string> + <string name="action_quick_reply">Schnell antworten</string> + <string name="action_reply">Antworten</string> + <string name="action_reblog">Teilen</string> + <string name="action_unreblog">Teilen rückgängig machen</string> + <string name="action_favourite">Favorisieren</string> + <string name="action_unfavourite">Aus Favoriten entfernen</string> + <string name="action_more">Mehr</string> + <string name="action_compose">Beitrag erstellen</string> + <string name="action_login">Mit Tusky anmelden</string> + <string name="action_logout">Abmelden</string> + <string name="action_logout_confirm">Möchtest du %1$s wirklich abmelden\? Dadurch werden alle lokalen Daten des Profils, wie Entwürfe und Einstellungen, gelöscht.</string> + <string name="action_follow">Folgen</string> + <string name="action_unfollow">Entfolgen</string> + <string name="action_block">Blockieren</string> + <string name="action_unblock">Blockierung aufheben</string> + <string name="action_hide_reblogs">Geteilte Beiträge ausblenden</string> + <string name="action_show_reblogs">Geteilte Beiträge anzeigen</string> + <string name="action_report">Melden</string> + <string name="action_delete">Löschen</string> + <string name="action_send">TRÖT</string> + <string name="action_send_public">TRÖT!</string> + <string name="action_retry">Erneut versuchen</string> + <string name="action_close">Schließen</string> + <string name="action_view_profile">Profil</string> + <string name="action_view_preferences">Einstellungen</string> + <string name="action_view_account_preferences">Profileinstellungen</string> + <string name="action_view_favourites">Favoriten</string> + <string name="action_view_mutes">Stummgeschaltete Profile</string> + <string name="action_view_blocks">Blockierte Profile</string> + <string name="action_view_follow_requests">Follower-Anfragen</string> + <string name="action_view_media">Medien</string> + <string name="action_open_in_web">Im Browser öffnen</string> + <string name="action_add_media">Mediendateien hinzufügen</string> + <string name="action_photo_take">Foto aufnehmen</string> + <string name="action_share">Teilen</string> + <string name="action_mute">Stummschalten</string> + <string name="action_unmute">Stummschaltung aufheben</string> + <string name="action_mention">Erwähnen</string> + <string name="action_hide_media">Mediendateien ausblenden</string> + <string name="action_open_drawer">Menü öffnen</string> + <string name="action_save">Speichern</string> + <string name="action_edit_profile">Profil bearbeiten</string> + <string name="action_edit_own_profile">Bearbeiten</string> + <string name="action_undo">Rückgängig machen</string> + <string name="action_accept">Akzeptieren</string> + <string name="action_reject">Ablehnen</string> + <string name="action_search">Suche</string> + <string name="action_access_drafts">Entwürfe</string> + <string name="action_toggle_visibility">Beitragssichtbarkeit</string> + <string name="action_content_warning">Inhaltswarnung</string> + <string name="action_emoji_keyboard">Emoji-Tastatur</string> + <string name="action_add_tab">Tab hinzufügen</string> + <string name="action_links">Links</string> + <string name="action_mentions">Erwähnungen</string> + <string name="action_hashtags">Hashtags</string> + <string name="action_open_reblogged_by">Geteilte Beiträge anzeigen</string> + <string name="action_open_faved_by">Favoriten anzeigen</string> + <string name="title_hashtags_dialog">Hashtags</string> + <string name="title_mentions_dialog">Erwähnungen</string> + <string name="title_links_dialog">Links</string> + <string name="action_open_media_n">Datei #%1$d öffnen</string> + <string name="download_image">%1$s heruntergeladen</string> + <string name="action_copy_link">Link kopieren</string> + <string name="action_open_as">Als %1$s öffnen</string> + <string name="action_share_as">Teilen als …</string> + <string name="send_post_link_to">Beitragslink teilen an …</string> + <string name="send_post_content_to">Beitrag teilen an …</string> + <string name="send_media_to">Mediendateien teilen an …</string> + <string name="confirmation_reported">Gesendet!</string> + <string name="confirmation_unblocked">Blockierung des Profils aufgehoben</string> + <string name="confirmation_unmuted">Stummschaltung des Profils aufgehoben</string> + <string name="hint_domain">Welche Instanz?</string> + <string name="hint_compose">Was gibt\'s Neues?</string> + <string name="hint_content_warning">Inhaltswarnung</string> + <string name="hint_display_name">Anzeigename</string> + <string name="hint_note">Über mich</string> + <string name="hint_search">Suchen …</string> + <string name="hint_media_description_missing">Medien sollten Beschreibungen haben.</string> + <string name="search_no_results">Keine Ergebnisse</string> + <string name="label_quick_reply">Antworten …</string> + <string name="label_avatar">Profilbild</string> + <string name="label_header">Titelbild</string> + <string name="link_whats_an_instance">Was ist eine Instanz\?</string> + <string name="login_connection">Wird verbunden …</string> + <string name="dialog_whats_an_instance">Die Adresse oder Domain einer Instanz kann hier eingegeben werden, wie z. B. mastodon.social, icosahedron.website, social.tchncs.de, and <a href="https://instances.social">more!</a> +\n +\nWenn du bis jetzt kein Konto hast, kannst du hier den Namen einer Instanz eingeben und dort ein Konto einrichten. +\n +\nEine Instanz ist ein einzelner Ort, an dem dein Konto gehostet ist, aber du kannst dennoch mit anderen Leuten interagieren, als wärt ihr alle auf derselben Website. +\n +\nWeitere Informationen gibt es auf <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="dialog_title_finishing_media_upload">Hochladen der Datei wird abgeschlossen</string> + <string name="dialog_message_uploading_media">Wird hochgeladen …</string> + <string name="dialog_download_image">Herunterladen</string> + <string name="dialog_message_cancel_follow_request">Follow-Anfrage zurückziehen?</string> + <string name="dialog_unfollow_warning">Dieses Profil entfolgen\?</string> + <string name="dialog_delete_post_warning">Diesen Beitrag löschen?</string> + <string name="visibility_public">Öffentlich: Für alle sichtbar</string> + <string name="visibility_unlisted">Nicht gelistet: Nicht in der öffentlichen Timeline sichtbar</string> + <string name="visibility_private">Nur Follower: Nur für Follower sichtbar</string> + <string name="visibility_direct">Direkt: Nur für die erwähnten Profile sichtbar</string> + <string name="pref_title_edit_notification_settings">Benachrichtigungen</string> + <string name="pref_title_notifications_enabled">Benachrichtigungen</string> + <string name="pref_title_notification_alerts">Benachrichtigungen</string> + <string name="pref_title_notification_alert_sound">Mit einem Ton benachrichtigen</string> + <string name="pref_title_notification_alert_vibrate">Mit Vibration benachrichtigen</string> + <string name="pref_title_notification_alert_light">Mit Licht benachrichtigen</string> + <string name="pref_title_notification_filters">Mich benachrichtigen, wenn</string> + <string name="pref_title_notification_filter_mentions">ich erwähnt werde</string> + <string name="pref_title_notification_filter_follows">mir jemand folgt</string> + <string name="pref_title_notification_filter_reblogs">meine Beiträge geteilt werden</string> + <string name="pref_title_notification_filter_favourites">meine Beiträge favorisiert werden</string> + <string name="pref_title_appearance_settings">Erscheinungsbild</string> + <string name="pref_title_app_theme">App-Design</string> + <string name="pref_title_timelines">Timelines</string> + <string name="pref_title_timeline_filters">Filter</string> + <string name="app_them_dark">Dunkel</string> + <string name="app_theme_light">Hell</string> + <string name="app_theme_black">Schwarz</string> + <string name="app_theme_auto">Automatisch bei Sonnenuntergang</string> + <string name="app_theme_system">Systemdesign verwenden</string> + <string name="pref_title_browser_settings">Browser</string> + <string name="pref_title_custom_tabs">Links in der App öffnen (Chrome Custom Tabs)</string> + <string name="pref_title_language">Sprache</string> + <string name="pref_title_post_filter">Timeline-Filter</string> + <string name="pref_title_post_tabs">Startseite Zeitleiste</string> + <string name="pref_title_show_boosts">Geteilte Beiträge anzeigen</string> + <string name="pref_title_show_replies">Antworten anzeigen</string> + <string name="pref_title_show_media_preview">Dateivorschauen herunterladen</string> + <string name="pref_title_proxy_settings">Proxy</string> + <string name="pref_title_http_proxy_settings">HTTP-Proxy</string> + <string name="pref_title_http_proxy_enable">HTTP-Proxy aktivieren</string> + <string name="pref_title_http_proxy_server">HTTP-Proxy-Server</string> + <string name="pref_title_http_proxy_port">HTTP-Proxy-Port</string> + <string name="pref_default_post_privacy">Standard-Beitragssichtbarkeit</string> + <string name="pref_default_media_sensitivity">Mediendateien immer mit einer Inhaltswarnung versehen</string> + <string name="pref_publishing">Wird veröffentlicht … (mit Server synchronisiert)</string> + <string name="pref_failed_to_sync">Fehler beim Synchronisieren</string> + <string name="post_privacy_public">Öffentlich</string> + <string name="post_privacy_unlisted">Nicht gelistet</string> + <string name="post_privacy_followers_only">Nur Follower</string> + <string name="pref_post_text_size">Schriftgröße von Beiträgen</string> + <string name="post_text_size_smallest">Kleiner</string> + <string name="post_text_size_small">Klein</string> + <string name="post_text_size_medium">Normal</string> + <string name="post_text_size_large">Groß</string> + <string name="post_text_size_largest">Größer</string> + <string name="notification_mention_name">Neue Erwähnungen</string> + <string name="notification_mention_descriptions">Benachrichtigungen über neue Erwähnungen</string> + <string name="notification_follow_name">Neue Follower</string> + <string name="notification_follow_description">Benachrichtigungen über neue Follower</string> + <string name="notification_boost_name">Geteilte Beiträge</string> + <string name="notification_boost_description">Benachrichtigungen, wenn deine Beiträge geteilt werden</string> + <string name="notification_favourite_name">Favorisierte Beiträge</string> + <string name="notification_favourite_description">Benachrichtigungen, wenn deine Beiträge favorisiert werden</string> + <string name="notification_mention_format">%1$s hat dich erwähnt</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s und %4$d andere</string> + <string name="notification_summary_medium">%1$s, %2$s, und %3$s</string> + <string name="notification_summary_small">%1$s und %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d neue Interaktion</item> + <item quantity="other">%1$d neue Interaktionen</item> + </plurals> + <string name="description_account_locked">Geschütztes Profil</string> + <string name="about_title_activity">Über</string> + <string name="about_tusky_license">Tusky ist eine freie und quelloffene Software und ist lizenziert unter der GNU General Public License Version 3. Du kannst die Lizenz hier einsehen: https://www.gnu.org/licenses/gpl-3.0.de.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">Website des Projekts: https://tusky.app</string> + <string name="about_bug_feature_request_site">Fehlermeldungen und Verbesserungsvorschläge: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Profil von Tusky</string> + <string name="post_share_content">Inhalt des Beitrags teilen</string> + <string name="post_share_link">Link zum Beitrag teilen</string> + <string name="post_media_images">Bilder</string> + <string name="post_media_video">Video</string> + <string name="state_follow_requested">Follower-Anfrage gesendet</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="follows_you">Folgt dir</string> + <string name="pref_title_alway_show_sensitive_media">Mediendateien mit Inhaltswarnung immer anzeigen</string> + <string name="title_media">Medien</string> + <string name="replying_to">Antwort an @%1$s</string> + <string name="load_more_placeholder_text">mehr laden</string> + <string name="pref_title_thread_filter_keywords">Unterhaltungen</string> + <string name="filter_addition_title">Filter hinzufügen</string> + <string name="filter_edit_title">Filter bearbeiten</string> + <string name="filter_dialog_remove_button">Entfernen</string> + <string name="filter_dialog_update_button">Aktualisieren</string> + <string name="add_account_name">Konto hinzufügen</string> + <string name="add_account_description">Neues Mastodon-Konto hinzufügen</string> + <string name="action_lists">Listen</string> + <string name="title_lists">Listen</string> + <string name="action_create_list">Liste erstellen</string> + <string name="action_rename_list">Liste aktualisieren</string> + <string name="action_delete_list">Liste löschen</string> + <string name="action_add_to_list">Konto zur Liste hinzufügen</string> + <string name="compose_active_account_description">Veröffentlichen als %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Inhalt für Menschen mit Sehbehinderung beschreiben (%1$d Zeichen)</item> + <item quantity="other">Inhalte für Menschen mit Sehbehinderung beschreiben (%1$d Zeichen)</item> + </plurals> + <string name="action_set_caption">Beschreibung eingeben</string> + <string name="action_remove">Entfernen</string> + <string name="lock_account_label">Geschütztes Profil</string> + <string name="lock_account_label_description">Follower müssen manuell genehmigt werden</string> + <string name="compose_save_draft">Entwurf speichern?</string> + <string name="send_post_notification_title">Beitrag wird gesendet …</string> + <string name="send_post_notification_error_title">Fehler beim Senden</string> + <string name="send_post_notification_channel_name">Beiträge senden</string> + <string name="send_post_notification_cancel_title">Senden abgebrochen</string> + <string name="send_post_notification_saved_content">Eine Kopie des Beitrags wurde in deine Entwürfe gespeichert</string> + <string name="action_compose_shortcut">Beitrag erstellen</string> + <string name="error_no_custom_emojis">Deine Instanz %1$s hat keine Emojis definiert</string> + <string name="emoji_style">Emoji-Stil</string> + <string name="system_default">Systemstandard</string> + <string name="download_fonts">Du musst diese Emoji-Sets zunächst herunterladen</string> + <string name="performing_lookup_title">Wird nachgeschlagen …</string> + <string name="expand_collapse_all_posts">Alle Beiträge aus-/einklappen</string> + <string name="action_open_post">Beitrag öffnen</string> + <string name="restart_required">App-Neustart erforderlich</string> + <string name="restart_emoji">Du musst Tusky neu starten, damit die Änderungen übernommen werden</string> + <string name="later">Später</string> + <string name="restart">Neu starten</string> + <string name="caption_systememoji">Die Standard-Emojis deines Geräts</string> + <string name="caption_blobmoji">Die Blob–Emojis aus Android 4.4–7.1</string> + <string name="caption_twemoji">Die Standard-Emojis von Mastodon</string> + <string name="caption_notoemoji">Die aktuellen Emojis von Google</string> + <string name="download_failed">Download fehlgeschlagen</string> + <string name="profile_badge_bot_text">Bot</string> + <string name="account_moved_description">%1$s ist umgezogen auf:</string> + <string name="reblog_private">An ursprüngliches Publikum teilen</string> + <string name="unreblog_private">Nicht mehr teilen</string> + <string name="license_description">Tusky enthält Code und Inhalte von den folgenden Open-Source-Projekten:</string> + <string name="license_apache_2">Lizenziert unter der Apache-Lizenz (s. u.)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Profil-Metadaten</string> + <string name="profile_metadata_add">Feld hinzufügen</string> + <string name="profile_metadata_label_label">Bezeichnung</string> + <string name="profile_metadata_content_label">Inhalt</string> + <string name="pref_title_absolute_time">Absolute Zeitstempel verwenden</string> + <string name="label_remote_account">Das Profil wird möglicherweise unvollständig wiedergegeben. Hier klicken, um das vollständige Profil im Browser zu öffnen.</string> + <string name="unpin_action">Vom Profil lösen</string> + <string name="pin_action">Im Profil anheften</string> + <string name="title_favourited_by">Favorisiert von</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s und %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s und %3$d mehr</string> + <string name="description_post_media_no_description_placeholder"> Keine Beschreibung </string> + <string name="description_post_favourited">Favorisiert </string> + <string name="description_visibility_public">Öffentlich </string> + <string name="description_visibility_private">Follower</string> + <string name="description_visibility_direct">Direkt </string> + <string name="hint_list_name">Listenname</string> + <string name="download_media">Dateien herunterladen</string> + <string name="downloading_media">Dateien werden heruntergeladen</string> + <string name="filter_add_description">zu filternder Ausdruck</string> + <string name="error_create_list">Liste konnte nicht erstellt werden</string> + <string name="error_rename_list">Liste konnte nicht aktualisiert werden</string> + <string name="error_delete_list">Liste konnte nicht gelöscht werden</string> + <string name="hint_search_people_list">Suche nach Leuten, denen du folgst</string> + <string name="action_remove_from_list">Konto aus der Liste entfernen</string> + <string name="edit_hashtag_hint">Hashtag ohne #</string> + <string name="action_open_reblogger">Autor*in des geteilten Beitrags öffnen</string> + <string name="pref_title_public_filter_keywords">Öffentliche Timelines</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Favorit</item> + <item quantity="other"><b>%1$s</b> Favoriten</item> + </plurals> + <string name="title_reblogged_by">Geteilt von</string> + <string name="description_post_media">Medien: %1$s</string> + <string name="description_post_cw">Inhaltswarnung: %1$s</string> + <string name="description_post_reblogged">Geteilt</string> + <string name="description_visibility_unlisted">Nicht gelistet</string> + <string name="action_delete_and_redraft">Löschen und neu erstellen</string> + <string name="dialog_redraft_post_warning">Diesen Beitrag löschen und neu erstellen\?</string> + <string name="pref_title_notification_filter_poll">Umfragen beendet wurden</string> + <string name="notification_poll_name">Umfragen</string> + <string name="notification_poll_description">Benachrichtigungen über beendete Umfragen</string> + <string name="notifications_clear">Löschen</string> + <string name="notifications_apply_filter">Filtern</string> + <string name="filter_apply">Übernehmen</string> + <string name="compose_shortcut_long_label">Beitrag erstellen</string> + <string name="compose_shortcut_short_label">Beitrag erstellen</string> + <string name="pref_title_bot_overlay">Hinweis für Bots anzeigen</string> + <string name="notification_clear_text">Bist du dir sicher, dass du alle deine Benachrichtigungen dauerhaft löschen möchtest\?</string> + <string name="poll_info_format"> \u0020<!-- 15 Stimmen • noch 1 Stunde --> \u0020%1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s Stimme</item> + <item quantity="other">%1$s Stimmen</item> + </plurals> + <string name="poll_info_time_absolute">endet um %1$s</string> + <string name="poll_info_closed">Geschlossen</string> + <string name="poll_vote">Abstimmen</string> + <string name="poll_ended_voted">Eine Umfrage, in der du abgestimmt hast, ist vorbei</string> + <string name="poll_ended_created">Eine Umfrage, die du erstellt hast, ist vorbei</string> + <plurals name="poll_timespan_days"> + <item quantity="one">noch %1$d Tag</item> + <item quantity="other">noch %1$d Tage</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">noch %1$d Stunde</item> + <item quantity="other">noch %1$d Stunden</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">noch %1$d Minute</item> + <item quantity="other">noch %1$d Minuten</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">noch %1$d Sekunde</item> + <item quantity="other">noch %1$d Sekunden</item> + </plurals> + <string name="title_domain_mutes">Ausgeblendete Domains</string> + <string name="action_view_domain_mutes">Ausgeblendete Domains</string> + <string name="action_mute_domain">%1$s stummschalten</string> + <string name="confirmation_domain_unmuted">%1$s nicht mehr ausgeblendet</string> + <string name="mute_domain_warning">Bist du dir sicher, dass du alles von %1$s blockieren möchtest\? Du wirst keine Inhalte dieser Domain in irgendwelchen öffentlichen Timelines oder in deinen Benachrichtigungen sehen. Deine Follower von dieser Domain werden entfernt.</string> + <string name="mute_domain_warning_dialog_ok">Gesamte Domain ausblenden</string> + <string name="pref_title_animate_gif_avatars">GIF-Profilbilder animieren</string> + <string name="filter_dialog_whole_word">Ganzes Wort</string> + <string name="filter_dialog_whole_word_description">Wenn das Wort oder die Formulierung nur aus Buchstaben oder Zahlen besteht, tritt der Filter nur dann in Kraft, wenn er exakt dieser Zeichenfolge entspricht</string> + <string name="button_continue">Weiter</string> + <string name="button_back">Zurück</string> + <string name="button_done">Fertig</string> + <string name="report_sent_success">\@%1$s wurde erfolgreich gemeldet</string> + <string name="hint_additional_info">Zusätzliche Anmerkungen</string> + <string name="report_remote_instance">An %1$s weiterleiten</string> + <string name="failed_report">Melden fehlgeschlagen</string> + <string name="report_description_1">Die Meldung wird an die Moderator*innen deines Servers geschickt. Du kannst hier eine Erklärung angeben, warum du dieses Konto meldest:</string> + <string name="report_description_remote_instance">Dieses Konto ist von einem anderen Server. Soll eine anonymisierte Kopie der Meldung auch dorthin geschickt werden\?</string> + <string name="create_poll_title">Umfrage</string> + <string name="duration_5_min">5 Minuten</string> + <string name="duration_30_min">30 Minuten</string> + <string name="duration_1_hour">1 Stunde</string> + <string name="duration_6_hours">6 Stunden</string> + <string name="duration_1_day">1 Tag</string> + <string name="duration_3_days">3 Tage</string> + <string name="duration_7_days">7 Tage</string> + <string name="edit_poll">Bearbeiten</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="action_add_poll">Umfrage hinzufügen</string> + <string name="pref_title_alway_open_spoiler">Beiträge mit Inhaltswarnungen immer ausklappen</string> + <string name="description_poll">Umfrage mit den Möglichkeiten: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="compose_preview_image_description">Aktionen für Bild %1$s</string> + <string name="failed_fetch_posts">Beiträge konnten nicht abgerufen werden</string> + <string name="title_accounts">Profile</string> + <string name="failed_search">Fehler beim Suchen</string> + <string name="add_poll_choice">Auswahlmöglichkeit hinzufügen</string> + <string name="poll_allow_multiple_choices">Mehrere Möglichkeiten</string> + <string name="poll_new_choice_hint">Möglichkeit %1$d</string> + <string name="title_scheduled_posts">Geplante Beiträge</string> + <string name="action_edit">Bearbeiten</string> + <string name="action_access_scheduled_posts">Geplante Beiträge</string> + <string name="action_schedule_post">Beitrag planen</string> + <string name="action_reset_schedule">Zurücksetzen</string> + <string name="title_bookmarks">Lesezeichen</string> + <string name="action_bookmark">Lesezeichen</string> + <string name="action_view_bookmarks">Lesezeichen</string> + <string name="about_powered_by_tusky">Unterstützt von Tusky</string> + <string name="description_post_bookmarked">Als Lesezeichen gespeichert</string> + <string name="select_list_title">Liste auswählen</string> + <string name="select_list_manage">Listen verwalten</string> + <string name="list">Liste</string> + <string name="post_lookup_error_format">Fehler beim Nachschlagen von %1$s</string> + <string name="no_drafts">Du hast keine Entwürfe.</string> + <string name="no_scheduled_posts">Du hast keine geplanten Beiträge.</string> + <string name="warning_scheduling_interval">Das Datum des geplanten Beitrags muss mindestens 5 Minuten in der Zukunft liegen.</string> + <string name="notification_follow_request_description">Benachrichtigungen über neue Follower-Anfragen</string> + <string name="pref_title_notification_filter_follow_requests">neue Follower-Anfrage</string> + <string name="notification_follow_request_name">Follower-Anfragen</string> + <string name="dialog_mute_warning">\@%1$s stummschalten\?</string> + <string name="dialog_block_warning">\@%1$s blockieren\?</string> + <string name="action_unmute_conversation">Stummschaltung der Unterhaltung aufheben</string> + <string name="action_mute_conversation">Unterhaltung stummschalten</string> + <string name="notification_follow_request_format">%1$s möchte dir folgen</string> + <string name="hashtags">Hashtags</string> + <string name="add_hashtag_title">Hashtag hinzufügen</string> + <string name="pref_title_confirm_reblogs">Vor dem Teilen bestätigen</string> + <string name="pref_title_show_cards_in_timelines">Linkvorschau in Timelines anzeigen</string> + <string name="pref_title_enable_swipe_for_tabs">Wischgeste zum Wechseln zwischen Tabs aktivieren</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s Person</item> + <item quantity="other">%1$s Personen</item> + </plurals> + <string name="pref_title_gradient_for_media">Farbverlauf für verborgene Medien anzeigen</string> + <string name="pref_main_nav_position_option_bottom">Unten</string> + <string name="pref_main_nav_position_option_top">Oben</string> + <string name="action_unmute_domain">%1$s nicht mehr stummschalten</string> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b>-mal geteilt</item> + <item quantity="other"><b>%1$s</b>-mal geteilt</item> + </plurals> + <string name="pref_main_nav_position">Position der Hauptnavigation</string> + <string name="dialog_mute_hide_notifications">Benachrichtigungen ausblenden</string> + <string name="action_unmute_desc">%1$s nicht mehr stummschalten</string> + <string name="abbreviated_seconds_ago">%1$d Sek.</string> + <string name="abbreviated_hours_ago">%1$d Std.</string> + <string name="abbreviated_in_days">in %1$d T.</string> + <string name="abbreviated_years_ago">%1$d J.</string> + <string name="account_note_saved">Gespeichert!</string> + <string name="account_note_hint">Private Notiz über dieses Konto</string> + <string name="pref_title_hide_top_toolbar">Titel der Hauptnavigation ausblenden</string> + <string name="no_announcements">Im Moment gibt es keine Ankündigungen.</string> + <string name="title_announcements">Ankündigungen</string> + <string name="drafts_post_reply_removed">Der Beitrag, auf den du antworten wolltest, wurde gelöscht</string> + <string name="draft_deleted">Entwurf gelöscht</string> + <string name="drafts_post_failed_to_send">Dieser Beitrag konnte nicht gesendet werden!</string> + <string name="dialog_delete_list_warning">Möchtest du die Liste %1$s wirklich löschen\?</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Du kannst nicht mehr als %1$d Anhang hochladen.</item> + <item quantity="other">Du kannst nicht mehr als %1$d Anhänge hochladen.</item> + </plurals> + <string name="pref_title_wellbeing_mode">Wohlbefinden</string> + <string name="label_duration">Dauer</string> + <string name="duration_indefinite">Für immer</string> + <string name="post_media_attachments">Anhänge</string> + <string name="post_media_audio">Audio</string> + <string name="notification_subscription_description">Benachrichtigungen, wenn jemand, den ich abonniert habe, eine neue Nachricht veröffentlicht</string> + <string name="notification_subscription_name">Neue Beiträge</string> + <string name="pref_title_animate_custom_emojis">GIF-Emojis animieren</string> + <string name="pref_title_notification_filter_subscriptions">jemand, den ich abonniert habe, etwas Neues veröffentlicht</string> + <string name="notification_subscription_format">%1$s hat gerade etwas veröffentlicht</string> + <string name="abbreviated_minutes_ago">%1$d Min.</string> + <string name="review_notifications">Benachrichtigungen überprüfen</string> + <string name="wellbeing_mode_notice">Informationen, die dein geistiges Wohlbefinden beeinflussen könnten, werden ausgeblendet. Dazu gehören: +\n +\n• Benachrichtigungen über favorisierte/geteilte Beiträge, sowie »X folgt dir«-Benachrichtigungen +\n• Anzahl der Favoriten von Beiträgen und wie oft diese geteilt wurden +\n• Statistiken zu Followern/Beiträgen auf Profilen +\n +\nPush-Benachrichtigungen sind nicht betroffen, aber du kannst deine Benachrichtigungseinstellungen manuell überprüfen.</string> + <string name="follow_requests_info">Auch wenn dein Konto öffentlich bzw. nicht geschützt ist, haben die Admins von %1$s gedacht, dass du diesen Follower lieber manuell bestätigen solltest.</string> + <string name="wellbeing_hide_stats_profile">Statistiken auf Profilen ausblenden</string> + <string name="wellbeing_hide_stats_posts">Statistiken in Beiträgen ausblenden</string> + <string name="limit_notifications">Timeline-Benachrichtigungen einschränken</string> + <string name="action_subscribe_account">Abonnieren</string> + <string name="action_unsubscribe_account">Deabonnieren</string> + <string name="abbreviated_in_minutes">in %1$d Min.</string> + <string name="abbreviated_in_hours">in %1$d Std.</string> + <string name="drafts_failed_loading_reply">Informationen zur Antwort konnten nicht geladen werden</string> + <string name="abbreviated_days_ago">%1$d T.</string> + <string name="abbreviated_in_years">in %1$d J.</string> + <string name="abbreviated_in_seconds">in %1$d Sek.</string> + <string name="action_unbookmark">Lesezeichen entfernen</string> + <string name="pref_title_confirm_favourites">Vor dem Favorisieren bestätigen</string> + <string name="dialog_delete_conversation_warning">Diese Unterhaltung wirklich löschen\?</string> + <string name="action_delete_conversation">Unterhaltung löschen</string> + <string name="duration_30_days">30 Tage</string> + <string name="duration_60_days">60 Tage</string> + <string name="duration_90_days">90 Tage</string> + <string name="duration_365_days">365 Tage</string> + <string name="duration_14_days">14 Tage</string> + <string name="duration_180_days">180 Tage</string> + <string name="tusky_compose_post_quicksetting_label">Beitrag erstellen</string> + <string name="notification_update_format">%1$s hat den Beitrag bearbeitet</string> + <string name="pref_title_notification_filter_updates">ein Beitrag, mit dem ich interagiert habe, bearbeitet wurde</string> + <string name="notification_sign_up_name">Registrierungen</string> + <string name="notification_sign_up_description">Benachrichtigungen über neue Profile</string> + <string name="notification_sign_up_format">%1$s hat sich registriert</string> + <string name="pref_title_notification_filter_sign_ups">jemand sich registriert</string> + <string name="notification_update_description">Benachrichtigungen, wenn Beiträge bearbeitet werden, mit denen du interagiert hast</string> + <string name="title_login">Anmelden</string> + <string name="error_could_not_load_login_page">Die Anmeldeseite konnte nicht geladen werden.</string> + <string name="notification_update_name">Beitragsbearbeitungen</string> + <string name="title_migration_relogin">Neuanmeldung für Push-Benachrichtigungen</string> + <string name="action_dismiss">Ablehnen</string> + <string name="dialog_push_notification_migration_other_accounts">Du hast dich erneut in dein aktuelles Konto angemeldet, um Tusky die Genehmigung für Push-Abonnements zu erteilen. Du hast jedoch noch andere Konten, die nicht auf diese Weise migriert wurden. Wechsel zu diesen Konten und melde dich nacheinander neu an, um die Unterstützung für UnifiedPush-Benachrichtigungen zu aktivieren.</string> + <string name="dialog_push_notification_migration">Um Push-Benachrichtigungen über UnifiedPush verwenden zu können, benötigt Tusky die Erlaubnis, Benachrichtigungen auf dem Mastodon-Server zu abonnieren. Dies erfordert eine erneute Anmeldung, um die Tusky gewährten OAuth-Bereiche zu ändern. Wenn du die Option zum erneuten Anmelden hier oder in den Profileinstellungen verwendest, bleiben alle deine lokalen Entwürfe und der Cache erhalten.</string> + <string name="tips_push_notification_migration">Melde alle Konten neu an, um die Unterstützung für Push-Benachrichtigungen zu aktivieren.</string> + <string name="account_date_joined">%1$s beigetreten</string> + <string name="status_count_one_plus">1+</string> + <string name="status_created_at_now">Jetzt</string> + <string name="error_loading_account_details">Fehler beim Laden der Kontodetails</string> + <string name="action_edit_image">Bild bearbeiten</string> + <string name="action_details">Details</string> + <string name="error_image_edit_failed">Das Bild konnte nicht bearbeitet werden.</string> + <string name="saving_draft">Entwurf wird gespeichert …</string> + <string name="error_multimedia_size_limit">Video- und Audiodateien dürfen nicht größer als %1$s MB sein.</string> + <string name="error_following_hashtag_format">Fehler beim Folgen von #%1$s</string> + <string name="error_unfollowing_hashtag_format">Fehler beim Entfolgen von #%1$s</string> + <string name="delete_scheduled_post_warning">Diesen geplanten Beitrag löschen\?</string> + <string name="instance_rule_title">%1$s-Regeln</string> + <string name="instance_rule_info">Mit dem Anmelden stimmst du den Regeln von %1$s zu.</string> + <string name="set_focus_description">Tippe oder ziehe den Kreis auf die Stelle, die in Vorschaubildern immer sichtbar sein soll.</string> + <string name="compose_save_draft_loses_media">Entwurf speichern\? (Anhänge werden erneut hochgeladen, sobald du den Entwurf wiederherstellst.)</string> + <string name="duration_no_change">(Keine Änderung)</string> + <string name="pref_title_show_self_username">Profilname in der Hauptnavigation anzeigen</string> + <string name="failed_to_pin">Anheften fehlgeschlagen</string> + <string name="failed_to_unpin">Lösen fehlgeschlagen</string> + <string name="pref_show_self_username_always">Immer</string> + <string name="pref_show_self_username_disambiguate">Wenn mit mehreren Konten angemeldet</string> + <string name="pref_show_self_username_never">Niemals</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="description_post_language">Sprache des Beitrags</string> + <string name="action_set_focus">Fokuspunkt setzen</string> + <string name="action_add_reaction">Reaktion hinzufügen</string> + <string name="failed_to_remove_from_list">Das Konto konnte nicht aus der Liste entfernt werden</string> + <string name="action_add_or_remove_from_list">Zur Liste hzfg. / aus Liste entf.</string> + <string name="failed_to_add_to_list">Das Konto konnte nicht zur Liste hinzugefügt werden</string> + <string name="no_lists">Du hast keine Listen.</string> + <string name="notification_summary_report_format">%1$s · %2$d Beiträge angehängt</string> + <string name="notification_header_report_format">%1$s meldete %2$s</string> + <string name="confirmation_hashtag_unfollowed">#%1$s entfolgt</string> + <string name="pref_default_post_language">Standard-Beitragssprache</string> + <string name="report_category_violation">Regelverstoß</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="report_category_spam">Spam</string> + <string name="report_category_other">Sonstiges</string> + <string name="error_following_hashtags_unsupported">Diese Instanz unterstützt das Folgen von Hashtags nicht.</string> + <string name="title_followed_hashtags">Gefolgte Hashtags</string> + <string name="action_unfollow_hashtag_format">#%1$s entfolgen\?</string> + <string name="description_post_edited">Bearbeitet</string> + <string name="a11y_label_loading_thread">Thread wird geladen …</string> + <string name="mute_notifications_switch">Benachrichtigungen stummschalten</string> + <string name="pref_reading_order_newest_first">Neueste zuerst</string> + <string name="pref_reading_order_oldest_first">Älteste zuerst</string> + <string name="pref_title_reading_order">Lesereihenfolge</string> + <string name="title_edits">Überarbeitete Beiträge</string> + <string name="pref_summary_http_proxy_disabled">Deaktiviert</string> + <string name="pref_summary_http_proxy_missing"><nicht gesetzt></string> + <string name="pref_summary_http_proxy_invalid"><ungültig></string> + <string name="compose_unsaved_changes">Du hast nicht gespeicherte Änderungen.</string> + <string name="pref_title_http_proxy_port_message">Port sollte zwischen %1$d und %2$d liegen</string> + <string name="error_muting_hashtag_format">Fehler beim Stummschalten von #%1$s</string> + <string name="action_post_failed">Hochladen fehlgeschlagen</string> + <string name="action_post_failed_detail">Dein Beitrag konnte nicht gepostet werden und wurde als Entwurf gespeichert. +\n +\nEntweder ist die Verbindung zum Server fehlgeschlagen oder der Server hat den Beitrag abgelehnt.</string> + <string name="action_post_failed_detail_plural">Deine Beiträge konnte nicht gepostet werden und wurde als Entwürfe gespeichert. +\n +\nEntweder ist die Verbindung zum Server fehlgeschlagen oder der Server hat die Beiträge abgelehnt.</string> + <string name="action_post_failed_show_drafts">Entwürfe anzeigen</string> + <string name="action_post_failed_do_nothing">Abbrechen</string> + <string name="post_edited">Bearbeitet %1$s</string> + <string name="action_discard">Änderungen verwerfen</string> + <string name="action_continue_edit">Bearbeiten fortsetzen</string> + <string name="action_browser_login">Mit Browser anmelden</string> + <string name="action_share_account_username">Name des Profils teilen</string> + <string name="action_share_account_link">Link zum Profil teilen</string> + <string name="send_account_link_to">Profil-Link teilen an …</string> + <string name="send_account_username_to">Profilname teilen an …</string> + <string name="account_username_copied">Profilname kopiert</string> + <string name="status_edit_info">Bearbeitet: %1$s</string> + <string name="status_created_info">Erstellt: %1$s</string> + <string name="notification_report_format">Neue Meldung über %1$s</string> + <string name="notification_report_description">Benachrichtigungen über Moderationsmeldungen</string> + <string name="pref_title_notification_filter_reports">es eine neue Meldung gibt</string> + <string name="error_unmuting_hashtag_format">Fehler beim Aufheben der Stummschaltung von #%1$s</string> + <string name="description_login">Funktioniert in den meisten Fällen. Keine Daten werden mit anderen Apps geteilt.</string> + <string name="description_browser_login">Zusätzliche Authentisierungsmethoden können unterstützt werden. Ein unterstützter Browser wird benötigt.</string> + <string name="notification_report_name">Meldungen</string> + <string name="error_status_source_load">Die Statusquelle konnte nicht vom Server geladen werden.</string> + <string name="title_public_trending_hashtags">Angesagte Hashtags</string> + <string name="accessibility_talking_about_tag">%1$d Leute schreiben über den Hashtag %2$s</string> + <string name="total_usage">Insg. verwendet</string> + <string name="total_accounts">Konten insg.</string> + <string name="dialog_follow_hashtag_title">Hashtag folgen</string> + <string name="dialog_follow_hashtag_hint">#Hashtag</string> + <string name="action_refresh">Aktualisieren</string> + <string name="socket_timeout_exception">Kontaktaufnahme mit dem Server dauerte zu lange</string> + <string name="ui_error_bookmark">Beitrag konnte nicht als Lesezeichen gespeichert werden: %1$s</string> + <string name="ui_error_reblog">Beitrag konnte nicht geteilt werden: %1$s</string> + <string name="ui_error_favourite">Beitrag konnte nicht favorisiert werden: %1$s</string> + <string name="ui_error_vote">Abstimmen bei der Umfrage schlug fehl: %1$s</string> + <string name="ui_error_accept_follow_request">Akzeptieren der Follower-Anfrage schlug fehl: %1$s</string> + <string name="ui_success_accepted_follow_request">Follower-Anfrage akzeptiert</string> + <string name="ui_success_rejected_follow_request">Follower-Anfrage blockiert</string> + <string name="notification_unknown_name">Unbekannt</string> + <string name="ui_error_unknown">Unbekannter Grund</string> + <string name="ui_error_clear_notifications">Löschen der Benachrichtigungen schlug fehl: %1$s</string> + <string name="ui_error_reject_follow_request">Ablehnen der Follower-Anfrage schlug fehl: %1$s</string> + <string name="status_filter_placeholder_label_format">Gefiltert: %1$s</string> + <string name="pref_title_account_filter_keywords">Profile</string> + <string name="hint_filter_title">Mein Filter</string> + <string name="label_filter_title">Titel</string> + <string name="filter_action_warn">Warnen</string> + <string name="filter_action_hide">Ausblenden</string> + <string name="filter_description_warn">Mit einer Warnung ausblenden</string> + <string name="filter_description_hide">Vollständig ausblenden</string> + <string name="label_filter_context">Nach Bereich filtern</string> + <string name="label_filter_action">Filteraktion</string> + <string name="label_filter_keywords">Schlagwörter oder Ausdrücke zum Filtern</string> + <string name="action_add">Hinzufügen</string> + <string name="filter_keyword_display_format">%1$s (ganzes Wort)</string> + <string name="filter_keyword_addition_title">Schlagwort hinzufügen</string> + <string name="filter_edit_keyword_title">Schlagwort bearbeiten</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="status_filtered_show_anyway">Trotzdem anzeigen</string> + <string name="help_empty_home">Dies ist deine <b>Startseite</b>. Sie zeigt die neuesten Beiträge von Konten, denen du folgst. +\n +\nDamit du andere Konten entdeckst, kannst du entweder andere Timelines lesen – z. B. die Lokale Timeline deiner Instanz [iconics gmd_group] – oder du suchst nach Namen [iconics gmd_search] – z. B. Tusky, um unser Mastodon-Konto zu finden.</string> + <string name="post_media_image">Bild</string> + <string name="pref_title_show_stat_inline">Beitragsstatistiken in der Timeline anzeigen</string> + <string name="pref_ui_text_size">Schriftgröße der Oberfläche</string> + <string name="notification_prune_cache">Cache-Wartung …</string> + <string name="notification_listenable_worker_description">Benachrichtigungen, wenn Tusky im Hintergrund aktiv ist</string> + <string name="notification_listenable_worker_name">Hintergrundaktivität</string> + <string name="notification_notification_worker">Benachrichtigungen werden abgerufen …</string> + <string name="load_newest_notifications">Neueste Benachrichtigungen laden</string> + <string name="compose_delete_draft">Entwurf löschen\?</string> + <string name="error_missing_edits">Deinem Server ist bekannt, dass dieser Beitrag bearbeitet wurde. Allerdings besitzt er keine Kopien der Änderungen, weshalb diese nicht angezeigt werden können. +\n +\nHierbei handelt es sich um <a href="https://github.com/mastodon/mastodon/issues/25398">Mastodon Issue #25398</a>.</string> + <string name="about_device_info_title">Dein Gerät</string> + <string name="about_device_info">%1$s %2$s +\nAndroid-Version: %3$s +\nSDK-Version: %4$d</string> + <string name="about_account_info_title">Dein Konto</string> + <string name="about_account_info">\@%1$s@%2$s +\nVersion: %3$s</string> + <string name="about_copied">Version und Geräteinformationen kopiert</string> + <string name="about_copy">Version und Geräteinformationen kopieren</string> + <string name="list_exclusive_label">Nicht in der Startseite anzeigen</string> + <string name="error_media_upload_sending_fmt">Das Hochladen ist fehlgeschlagen: %1$s</string> + <string name="label_image">Bild</string> + <string name="help_empty_lists">Das ist deine <b>Listenansicht</b>. Du kannst private Listen erstellen und diese Profile hinzufügen. +\n +\n BEACHTE, dass du nur Profile hinzufügen kannst, denen du folgst. +\n +\n Diese Listen können als Tab in den Kontoeinstellungen [iconics gmd_account_circle] [iconics gmd_navigate_next] Tabs festgelegt werden. </string> + <string name="help_empty_conversations">Hier befinden sich deine <b>privaten Nachrichten</b> – auch bekannt als Unterhaltungen oder »direct messages« (DM, Direktnachricht). +\n +\nPrivate Nachrichten werden erstellt, indem die Beitragssichtbarkeit [iconics gmd_public] auf [iconics gmd_mail] <i>Direkt</i> gesetzt wird und ein oder mehrere Profile erwähnt werden. +\n +\nBeispiel: Du befindest dich auf einer Profilseite und klickst auf die Schaltfläche »Verfassen« [iconics gmd_edit] und änderst anschließend die Sichtbarkeit. </string> + <string name="app_theme_system_black">Systemdesign verwenden (Schwarz)</string> + <string name="error_media_playback">Wiedergabe fehlgeschlagen: %1$s</string> + <string name="dialog_delete_filter_positive_action">Entfernen</string> + <string name="dialog_delete_filter_text">Filter »%1$s« entfernen\?</string> + <string name="dialog_save_profile_changes_message">Möchtest du deine Änderungen am Profil speichern\?</string> + <string name="unmuting_hashtag_success_format">Hashtag #%1$s nicht mehr stummschalten</string> + <string name="action_view_filter">Filter ansehen</string> + <string name="muting_hashtag_success_format">Hashtag #%1$s als Warnung stummschalten</string> + <string name="following_hashtag_success_format">Hashtag #%1$s wird jetzt gefolgt</string> + <string name="unfollowing_hashtag_success_format">Hashtag #%1$s wird nicht länger gefolgt</string> + <string name="title_public_trending_statuses">Angesagte Beiträge</string> + <string name="error_blocking_domain">Fehler beim Stummschalten von %1$s: %2$s</string> + <string name="error_unblocking_domain">Fehler beim Aufheben der Stummschaltung von %1$s: %2$s</string> + <string name="list_reply_policy_label">Antworten anzeigen für</string> + <string name="list_reply_policy_followed">Alle gefolgten Profile</string> + <string name="list_reply_policy_list">Mitglieder der Liste</string> + <string name="list_reply_policy_none">Niemanden</string> + <string name="pref_title_show_self_boosts">Eigene geteilte Beiträge anzeigen</string> + <string name="pref_title_show_self_boosts_description">Die Person teilt ihre eigenen Beiträge</string> + <string name="pref_title_per_timeline_preferences">Einstellungen pro Zeitleiste</string> + <string name="reply_sending">Wird gesendet …</string> + <string name="reply_sending_long">Deine Antwort wird gesendet.</string> + <string name="action_translate">Übersetzen</string> + <string name="action_show_original">Ursprünglichen Beitrag anzeigen</string> + <string name="label_translating">Wird übersetzt …</string> + <string name="label_translated">Aus %1$s mittels %2$s übersetzt</string> + <string name="ui_error_translate">Fehler beim Übersetzen: %1$s</string> + <string name="pref_title_show_notifications_filter">Benachrichtigungsfilter anzeigen</string> + <string name="report_category_legal">Rechtliches</string> + <string name="url_copied">URL kopiert</string> + <string name="confirmation_hashtag_copied">»%1$s« kopiert</string> + <string name="pref_title_confirm_follows">Vor dem Folgen bestätigen</string> + <string name="unknown_notification_type">Unbekannter Nachrichtentyp</string> + <string name="dialog_follow_warning">Diesem Konto folgen?</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-el/strings.xml b/app/src/main/res/values-el/strings.xml new file mode 100644 index 0000000..dd58361 --- /dev/null +++ b/app/src/main/res/values-el/strings.xml @@ -0,0 +1,176 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_empty">Αυτό δεν μπορεί να είναι άδειο.</string> + <string name="error_network">Προέκυψε σφάλμα δικτύου! Παρακαλώ ελέγξτε τη σύνδεσή σας και προσπαθήστε ξανά.</string> + <string name="error_generic">Προέκυψε ένα σφάλμα.</string> + <string name="action_view_blocks">Αποκλεισμένοι χρήστες</string> + <string name="dialog_message_cancel_follow_request">Ακύρωση αιτήματος ακολούθησης;</string> + <string name="dialog_delete_conversation_warning">Διαγραφή αυτής της συζήτησης;</string> + <string name="search_no_results">Δεν υπάρχουν αποτελέσματα</string> + <string name="title_edit_profile">Επεργασία προφίλ</string> + <string name="title_follows">Ακολουθεί</string> + <string name="action_reset_schedule">Επαναφορά</string> + <string name="notification_follow_format">%1$s σας ακολούθησε</string> + <string name="action_view_mutes">Χρήστες σε σίγαση</string> + <string name="action_logout">Αποσύνδεση</string> + <string name="action_unfollow">Παύω να ακολουθώ</string> + <string name="action_unmute_domain">Άρση σίγασης %1$s</string> + <string name="action_delete_and_redraft">Διαγραφή και αναδιατύπωση</string> + <string name="action_edit_profile">Επεξεργασία προφίλ</string> + <string name="action_share">Κοινοποίηση</string> + <string name="title_licenses">Άδειες</string> + <string name="action_open_in_web">Ανοίξτε σε περιηγητή</string> + <string name="action_view_follow_requests">Αιτήματα ακολούθησης</string> + <string name="action_bookmark">Προσθήκη σελιδοδείκτη</string> + <string name="action_more">Περισσότερα</string> + <string name="action_view_bookmarks">Σελιδοδείκτες</string> + <string name="title_bookmarks">Σελιδοδείκτες</string> + <string name="title_followers">Ακόλουθοι</string> + <string name="action_unblock">Άρση αποκλεισμού</string> + <string name="title_favourites">Αγαπημένα</string> + <string name="error_compose_character_limit">Η δημοσίευση είναι πολύ μεγάλη!</string> + <string name="action_emoji_keyboard">Πληκτρολόγιο emoji</string> + <string name="action_logout_confirm">Σίγουρα θέλετε να αποσυνδεθείτε από το %1$s; Αυτό θα διαγράψει όλα τα τοπικά δεδομένα του λογαριασμού, συμπεριλαμβανομένων των πρόχειρων και των προτιμήσεων.</string> + <string name="title_drafts">Πρόχειρα</string> + <string name="action_view_favourites">Αγαπημένα</string> + <string name="label_quick_reply">Απάντηση…</string> + <string name="action_reject">Απόρριψη</string> + <string name="title_blocks">Αποκλεισμένοι χρήστες</string> + <string name="action_unreblog">Αφαίρεση προώθησης</string> + <string name="action_edit">Επεξεργασία</string> + <string name="action_mute_domain">Σίγαση %1$s</string> + <string name="action_block">Αποκλεισμός</string> + <string name="action_undo">Αναίρεση</string> + <string name="notification_follow_request_format">%1$s ζήτησε να σας ακολουθήσει</string> + <string name="action_reply">Απάντηση</string> + <string name="title_tab_preferences">Καρτέλες</string> + <string name="notification_reblog_format">%1$s προώθησε τη δημοσίευσή σας</string> + <string name="notification_favourite_format">%1$s προτίμησε τη δημοσίευσή σας</string> + <string name="action_follow">Ακολουθήστε</string> + <string name="action_report">Αναφορά</string> + <string name="action_mute">Σίγαση</string> + <string name="action_unfavourite">Αφαίρεση αγαπημένου</string> + <string name="report_username_format">Αναφορά %1$s</string> + <string name="action_view_account_preferences">Προτιμήσεις λογαριασμού</string> + <string name="action_add_tab">Προσθήκη καρτέλας</string> + <string name="action_copy_link">Αντιγραφή συνδέσμου</string> + <string name="hint_search">Αναζήτηση…</string> + <string name="action_accept">Αποδοχή</string> + <string name="action_show_reblogs">Εμφάνιση προωθήσεων</string> + <string name="action_view_profile">Προφίλ</string> + <string name="title_follow_requests">Αιτήματα ακολούθησης</string> + <string name="action_search">Αναζήτηση</string> + <string name="action_delete_conversation">Διαγραφή συζήτησης</string> + <string name="action_delete">Διαγραφή</string> + <string name="notification_subscription_format">%1$s μόλις δημοσίευσε</string> + <string name="action_save">Αποθήκευση</string> + <string name="action_quick_reply">Γρήγορη Απάντηση</string> + <string name="title_mutes">Χρήστες σε σίγαση</string> + <string name="action_hide_reblogs">Απόκρυψη προωθήσεων</string> + <string name="action_view_preferences">Προτιμήσεις</string> + <string name="title_login">Σύνδεση</string> + <string name="title_announcements">Ανακοινώσεις</string> + <string name="action_access_drafts">Πρόχειρα</string> + <string name="notification_sign_up_format">%1$s εγγράφηκε</string> + <string name="action_retry">Προσπαθήστε ξανά</string> + <string name="dialog_delete_post_warning">Διαγραφή αυτής της δημοσίευσης;</string> + <string name="action_unmute">Άρση σίγασης</string> + <string name="action_favourite">Αγαπημένο</string> + <string name="title_links_dialog">Σύνδεσμοι</string> + <string name="action_close">Κλείσιμο</string> + <string name="title_notifications">Ειδοποιήσεις</string> + <string name="action_compose">Συνθέστε</string> + <string name="action_login">Σύνδεση με Tusky</string> + <string name="action_edit_own_profile">Επεξεργασία</string> + <string name="action_reblog">Προώθηση</string> + <string name="dialog_unfollow_warning">Άρση ακολούθησης αυτού του λογαριασμού;</string> + <string name="dialog_mute_hide_notifications">Απόκρυψη ειδοποιήσεων</string> + <string name="action_unbookmark">Αφαίρεση σελιδοδείκτη</string> + <string name="action_content_warning">Προειδοποίηση περιεχομένου</string> + <string name="action_links">Σύνδεσμοι</string> + <string name="login_connection">Σύνδεση…</string> + <string name="action_access_scheduled_posts">Προγραμματισμένες δημοσιεύσεις</string> + <string name="action_schedule_post">Προγραμματισμός δημοσίευσης</string> + <string name="title_scheduled_posts">Προγραμματισμένες δημοσιεύσεις</string> + <string name="title_posts">Δημοσιεύσεις</string> + <string name="title_posts_pinned">Καρφιτσωμένο</string> + <string name="post_sensitive_media_title">Ευαίσθητο περιεχόμενο</string> + <string name="post_media_hidden_title">Κρυφά πολυμέσα</string> + <string name="post_boosted_format">%1$s προώθησε</string> + <string name="title_posts_with_replies">Με απαντήσεις</string> + <string name="post_content_warning_show_more">Δείτε περισσότερα</string> + <string name="post_content_warning_show_less">Δείτε λιγότερα</string> + <string name="post_sensitive_media_directions">Πατήστε για να δείτε</string> + <string name="notification_update_format">%1$s επεξεργάσταν τη δημοσίευσή τους</string> + <string name="dialog_redraft_post_warning">Διαγραφή και αναδιατύπωση αυτής της δημοσίευσης;</string> + <string name="error_following_hashtag_format">Σφάλμα ακολούθησης #%1$s</string> + <string name="error_unfollowing_hashtag_format">Σφάλμα παύσης ακολούθησης #%1$s</string> + <string name="error_media_upload_sending_fmt">Το ανέβασμα απέτυχε: %1$s</string> + <string name="error_media_upload_sending">Το ανέβασμα αρχείου απέτυχε.</string> + <string name="error_loading_account_details">Η φόρτωση λεπτομερειών λογαριασμού απέτυχε</string> + <string name="title_home">Αρχική σελίδα</string> + <string name="error_sender_account_gone">Σφάλμα αποστολής ανάρτησης.</string> + <string name="error_could_not_load_login_page">Δεν ήταν δυνατή η φόρτωση της σελίδας εισόδου χρήστη.</string> + <string name="account_note_hint">Το προσωπικό σας σημείωμα σχετικά με αυτόν τον λογαριασμό</string> + <string name="description_post_media_no_description_placeholder">Χωρίς περιγραφή</string> + <string name="delete_scheduled_post_warning">Διαγραφή της προγραμματισμένης ανάρτησης;</string> + <string name="post_media_attachments">Συνημμένα</string> + <string name="hint_media_description_missing">Τα πολυμέσα πρέπει να έχουν μια περιγραφή.</string> + <string name="error_multimedia_size_limit">Το μέγεθος των αρχείων βίντεο και ήχου δεν μπορεί να υπερβαίνει τα %1$s MB.</string> + <string name="error_media_upload_permission">Απαιτείται άδεια ανάγνωσης των πολυμέσων.</string> + <string name="notification_mention_descriptions">Ειδοποιήσεις για νέες μνείες</string> + <string name="action_hide_media">Απόκρυψη πολυμέσων</string> + <string name="downloading_media">Γίνεται λήψη πολυμέσων</string> + <string name="download_media">Λήψη πολυμέσων</string> + <string name="action_lists">Λίστες</string> + <string name="action_delete_list">Διαφραφή λίστας</string> + <string name="notification_follow_description">Ειδοποιήσεις για νέους ακόλουθους</string> + <string name="title_accounts">Λογαριασμοί</string> + <string name="about_device_info_title">Η συσκευή σου</string> + <string name="dialog_delete_list_warning">Θέλετε πράγματι να διαγράψετε τη λίστα %1$s;</string> + <string name="about_copied">Αντιγραμμένη έκδοση και πληροφορίες συσκευής</string> + <string name="action_add_media">Προσθήκη πολυμέσων</string> + <string name="send_media_to">Κοινοποίηση πολυμέσων σε…</string> + <string name="post_media_image">Εικόνα</string> + <string name="post_media_alt">ALT</string> + <string name="draft_deleted">Πρόχειρο διαγράφηκε</string> + <string name="action_view_media">Πολυμέσα</string> + <string name="dialog_message_uploading_media">Ανέβασμα…</string> + <string name="about_title_activity">Σχετικά</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="pref_title_alway_show_sensitive_media">Να εμφανίζετε πάντα το ευαίσθητο περιεχόμενο</string> + <string name="description_post_media">Πολυμέσα: %1$s</string> + <string name="title_mentions_dialog">Μνείες</string> + <string name="title_hashtags_dialog">Ετικέτες</string> + <string name="dialog_title_finishing_media_upload">Τέλος ανεβάσματος πολυμέσων</string> + <string name="notification_mention_name">Νέες μνείες</string> + <string name="no_lists">Δεν έχετε λίστες.</string> + <string name="pref_title_notification_filter_mentions">έγινε μνεία</string> + <string name="about_copy">Αντιγραφή έκδοσης και πληροφοριών συσκευής</string> + <string name="dialog_delete_filter_positive_action">Διαγραφή</string> + <string name="select_list_manage">Διαχείρηση λιστών</string> + <string name="title_public_trending_statuses">Δημοφιλείς δημοσιεύσεις</string> + <string name="title_public_trending_hashtags">Δημοφιλείς ετικέτες</string> + <string name="notifications_clear">Διαγραφή</string> + <string name="action_mentions">Μνείες</string> + <string name="title_media">Πολυμέσα</string> + <string name="compose_delete_draft">Διαγραφή πρόχειρου;</string> + <string name="title_lists">Λίστες</string> + <string name="error_delete_list">Δεν μπόρεσε να διαγράψει τη λίστα</string> + <string name="post_media_images">Εικόνες</string> + <string name="post_media_video">Βίντεο</string> + <string name="post_media_audio">Ήχος</string> + <string name="about_account_info_title">Ο λογαρισμός σου</string> + <string name="about_project_site">Ιστοσελίδα εφαρμογής: https://tusky.app</string> + <string name="action_open_media_n">Άνοιγμα πολυμέσων #%1$d</string> + <string name="dialog_delete_filter_text">Διαγραφή φίλτρου \'%1$s\';</string> + <string name="error_media_upload_type">Αυτός ο τύπος αρχείου δεν μπορεί να μεταφορτωθεί.</string> + <string name="error_media_upload_opening">Αυτό το αρχείο δεν μπόρεσε να ανοίξει.</string> + <string name="title_direct_messages">Απευθείας μηνύματα</string> + <string name="title_public_local">Τοπικά</string> + <string name="error_no_web_browser_found">Δεν βρέθηκε πρόγραμμα περιήγησης στο διαδίκτυο για χρήση.</string> + <string name="error_media_download_permission">Απαιτείται άδεια για την αποθήκευση πολυμέσων.</string> + <string name="error_image_edit_failed">Η εικόνα δεν μπορεί να επεξεργαστεί.</string> + <string name="unknown_notification_type">Άγνωστος τύπος ειδοποιήσης</string> + <string name="list_reply_policy_list">Μέλη της λίστας</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-en-rAU/strings.xml b/app/src/main/res/values-en-rAU/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/main/res/values-en-rAU/strings.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources></resources> \ No newline at end of file diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml new file mode 100644 index 0000000..5cb9353 --- /dev/null +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="report_description_remote_instance">The account is from another server. Send an anonymised copy of the report there as well\?</string> + <string name="description_post_favourited">Favourited</string> + <string name="title_favourited_by">Favourited by</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Favourite</item> + <item quantity="other"><b>%1$s</b> Favourites</item> + </plurals> + <string name="notification_favourite_description">Notifications when your posts get marked as favourite</string> + <string name="notification_favourite_name">Favourites</string> + <string name="pref_title_notification_filter_favourites">my posts are favourited</string> + <string name="action_open_faved_by">Show favourites</string> + <string name="action_view_favourites">Favourites</string> + <string name="action_unfavourite">Remove favourite</string> + <string name="action_favourite">Favourite</string> + <string name="notification_favourite_format">%1$s favourited your post</string> + <string name="title_favourites">Favourites</string> + <string name="error_authorization_denied">Authorisation was denied. If you\'re sure that you supplied the correct credentials, try Login in Browser from the menu.</string> + <string name="error_authorization_unknown">An unidentified authorisation error occurred.</string> + <string name="pref_title_gradient_for_media">Show colourful gradients for hidden media</string> +</resources> diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml new file mode 100644 index 0000000..5a7fb2c --- /dev/null +++ b/app/src/main/res/values-eo/strings.xml @@ -0,0 +1,557 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Eraro okazis.</string> + <string name="error_network">Reta eraro okazis! Bonvolu kontroli vian konekton kaj klopodi denove!</string> + <string name="error_empty">Tiu ne povas esti malplena.</string> + <string name="error_invalid_domain">Enmetita domajno estas nevalida</string> + <string name="error_failed_app_registration">Aŭtentigo en ĉi tiu nodo malsukcesis.</string> + <string name="error_no_web_browser_found">Ne eblas trovi retumilon.</string> + <string name="error_authorization_unknown">Nekonata eraro de aŭtentigo okazis.</string> + <string name="error_authorization_denied">Rajtigo rifuzita.</string> + <string name="error_retrieving_oauth_token">Akiro de atingoĵetono malsukcesis.</string> + <string name="error_compose_character_limit">Via mesaĝo estas tro longa!</string> + <string name="error_media_upload_type">Tia dosiero ne estas alŝutebla.</string> + <string name="error_media_upload_opening">Tiu dosiero ne povas esti malfermita.</string> + <string name="error_media_upload_permission">Permeso legi aŭdovidaĵojn necesas.</string> + <string name="error_media_download_permission">Permeso konservi aŭdovidaĵojn necesas.</string> + <string name="error_media_upload_image_or_video">Bildoj kaj videoj ne povas esti ambaŭ alkroĉitaj al la sama mesaĝo.</string> + <string name="error_media_upload_sending">La alŝuto malsukcesis.</string> + <string name="error_sender_account_gone">Okazis eraro dum la sendo de la mesaĝo.</string> + <string name="title_home">Hejmo</string> + <string name="title_notifications">Sciigoj</string> + <string name="title_public_local">Loka</string> + <string name="title_public_federated">Fratara</string> + <string name="title_direct_messages">Rektaj mesaĝoj</string> + <string name="title_tab_preferences">Langetoj</string> + <string name="title_view_thread">Fadeno</string> + <string name="title_posts">Mesaĝoj</string> + <string name="title_posts_with_replies">Kun respondoj</string> + <string name="title_posts_pinned">Alpinglitaj</string> + <string name="title_follows">Sekvatoj</string> + <string name="title_followers">Sekvantoj</string> + <string name="title_favourites">Stelumoj</string> + <string name="title_mutes">Silentigitaj uzantoj</string> + <string name="title_blocks">Blokitaj uzantoj</string> + <string name="title_follow_requests">Petoj de sekvado</string> + <string name="title_edit_profile">Redakti vian profilon</string> + <string name="title_drafts">Malnetoj</string> + <string name="title_licenses">Permesiloj</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s diskonigis</string> + <string name="post_sensitive_media_title">Tikla enhavo</string> + <string name="post_media_hidden_title">Kaŝitaj aŭdovidaĵoj</string> + <string name="post_sensitive_media_directions">Alklaki por vidi</string> + <string name="post_content_warning_show_more">Montri pli</string> + <string name="post_content_warning_show_less">Montri malpli</string> + <string name="post_content_show_more">Pligrandigi</string> + <string name="post_content_show_less">Malgrandigi</string> + <string name="message_empty">Nenio tie ĉi.</string> + <string name="footer_empty">Nenio tie ĉi. Tiru malsupren por aktualigi!</string> + <string name="notification_reblog_format">%1$s diskonigis vian mesaĝon</string> + <string name="notification_favourite_format">%1$s stelumis vian mesaĝon</string> + <string name="notification_follow_format">%1$s eksekvis vin</string> + <string name="report_username_format">Signali @%1$s</string> + <string name="report_comment_hint">Ĉu pliaj komentoj\?</string> + <string name="action_quick_reply">Rapida respondo</string> + <string name="action_reply">Respondi</string> + <string name="action_reblog">Diskonigi</string> + <string name="action_unreblog">Nuligi diskonigon</string> + <string name="action_favourite">Stelumi</string> + <string name="action_unfavourite">Nuligi stelumon</string> + <string name="action_more">Pli</string> + <string name="action_compose">Verki</string> + <string name="action_login">Ensaluti al Mastodon</string> + <string name="action_logout">Elsaluti</string> + <string name="action_logout_confirm">Ĉu vi certas, ke vi volas elsaluti el la konto %1$s\?</string> + <string name="action_follow">Sekvi</string> + <string name="action_unfollow">Ne plu sekvi</string> + <string name="action_block">Bloki</string> + <string name="action_unblock">Malbloki</string> + <string name="action_hide_reblogs">Kaŝi diskonigojn</string> + <string name="action_show_reblogs">Montri diskonigojn</string> + <string name="action_report">Signali</string> + <string name="action_delete">Forigi</string> + <string name="action_send">HUP</string> + <string name="action_send_public">HUP!</string> + <string name="action_retry">Reprovi</string> + <string name="action_close">Fermi</string> + <string name="action_view_profile">Profilo</string> + <string name="action_view_preferences">Agordoj</string> + <string name="action_view_account_preferences">Agordoj de konto</string> + <string name="action_view_favourites">Stelumoj</string> + <string name="action_view_mutes">Silentigitaj uzantoj</string> + <string name="action_view_blocks">Blokitaj uzantoj</string> + <string name="action_view_follow_requests">Petoj de sekvado</string> + <string name="action_view_media">Aŭdovidaĵo</string> + <string name="action_open_in_web">Malfermi en retumilo</string> + <string name="action_add_media">Aldoni aŭdovidaĵon</string> + <string name="action_photo_take">Foti</string> + <string name="action_share">Konigi</string> + <string name="action_mute">Silentigi</string> + <string name="action_unmute">Malsilentigi</string> + <string name="action_mention">Mencii</string> + <string name="action_hide_media">Kaŝi aŭdovidaĵojn</string> + <string name="action_open_drawer">Montri tirmenuon</string> + <string name="action_save">Konservi</string> + <string name="action_edit_profile">Redakti profilon</string> + <string name="action_edit_own_profile">Redakti</string> + <string name="action_undo">Malfari</string> + <string name="action_accept">Rajtigi</string> + <string name="action_reject">Rifuzi</string> + <string name="action_search">Serĉi</string> + <string name="action_access_drafts">Malnetoj</string> + <string name="action_toggle_visibility">Videblo de la mesaĝo</string> + <string name="action_content_warning">Enhava averto</string> + <string name="action_emoji_keyboard">Klavaro de emoĝioj</string> + <string name="action_add_tab">Aldoni langeton</string> + <string name="action_links">Ligiloj</string> + <string name="action_mentions">Mencioj</string> + <string name="action_hashtags">Kradvortoj</string> + <string name="action_open_reblogger">Montri la aŭtoron de la diskonigo</string> + <string name="action_open_reblogged_by">Montri diskonigojn</string> + <string name="action_open_faved_by">Montri stelumojn</string> + <string name="title_hashtags_dialog">Kradvortoj</string> + <string name="title_mentions_dialog">Mencioj</string> + <string name="title_links_dialog">Ligiloj</string> + <string name="action_open_media_n">Malfermi aŭdovidaĵon #%1$d</string> + <string name="download_image">Elŝutado de %1$s</string> + <string name="action_copy_link">Kopii la ligilon</string> + <string name="action_open_as">Malfermi kiel %1$s</string> + <string name="action_share_as">Konigi kiel…</string> + <string name="download_media">Elŝuti aŭdovidaĵon</string> + <string name="downloading_media">Elŝutado de la aŭdovidaĵo</string> + <string name="send_post_link_to">Konigi ligilon de mesaĝo al…</string> + <string name="send_post_content_to">Konigi mesaĝon al…</string> + <string name="send_media_to">Konigi aŭdovidaĵon al…</string> + <string name="confirmation_reported">Sendita!</string> + <string name="confirmation_unblocked">Malblokita uzanto</string> + <string name="confirmation_unmuted">Malsilentigita uzanto</string> + <string name="hint_domain">Kiu nodo?</string> + <string name="hint_compose">Kio nova\?</string> + <string name="hint_content_warning">Enhava averto</string> + <string name="hint_display_name">Publika nomo</string> + <string name="hint_note">Sinprezento</string> + <string name="hint_search">Serĉi…</string> + <string name="search_no_results">Neniu rezulto</string> + <string name="label_quick_reply">Respondi…</string> + <string name="label_avatar">Profilbildo</string> + <string name="label_header">Fonbildo</string> + <string name="link_whats_an_instance">Kio estas nodo?</string> + <string name="login_connection">Konektado…</string> + <string name="dialog_whats_an_instance">La adreso aŭ domajno de iu ajn nodo povas esti enmetita ĉi tie, kiel mastodon.social, icosahedron.website, social.tchncs.de, kaj <a href="https://instances.social">pli!</a> +\n +\nSe vi ankoraŭ ne havas konton, vi povas enmeti la nomon de la nodo, al kiu vi volas aliĝi, kaj krei konton tie. +\n +\nNodo estas unika loko tie, kie via konto estas gastigita, sed vi povas facile komuniki kun homoj, kaj sekvi ilin ĉe aliaj nodoj, kvazaŭ vi estus samreteje. +\n +\nPliaj informoj troviĝas ĉe <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="dialog_title_finishing_media_upload">Finante alŝuto de aŭdovidaĵojn</string> + <string name="dialog_message_uploading_media">Alŝutado…</string> + <string name="dialog_download_image">Elŝuti</string> + <string name="dialog_message_cancel_follow_request">Ĉu nuligi peton de sekvado\?</string> + <string name="dialog_unfollow_warning">Ĉu ne plu sekvi\?</string> + <string name="dialog_delete_post_warning">Ĉu forigi ĉi tiun mesaĝon\?</string> + <string name="visibility_public">Publika: afiŝi en publikaj tempolinioj</string> + <string name="visibility_unlisted">Nelistigita: Ne afiŝi en publikaj tempolinioj</string> + <string name="visibility_private">Nur por sekvantoj: Afiŝi nur al sekvantoj</string> + <string name="visibility_direct">Rekta: Afiŝi nur al menciitaj uzantoj</string> + <string name="pref_title_edit_notification_settings">Sciigoj</string> + <string name="pref_title_notifications_enabled">Sciigoj</string> + <string name="pref_title_notification_alerts">Avertoj</string> + <string name="pref_title_notification_alert_sound">Sciigi per sono</string> + <string name="pref_title_notification_alert_vibrate">Sciigi per vibro</string> + <string name="pref_title_notification_alert_light">Sciigi per lumo</string> + <string name="pref_title_notification_filters">Sciigi al mi, kiam</string> + <string name="pref_title_notification_filter_mentions">iu mencias min</string> + <string name="pref_title_notification_filter_follows">iu sekvas min</string> + <string name="pref_title_notification_filter_reblogs">miaj mesaĝoj estas diskonigitaj</string> + <string name="pref_title_notification_filter_favourites">miaj mesaĝoj estas stelumitaj</string> + <string name="pref_title_appearance_settings">Aspekto</string> + <string name="pref_title_app_theme">Temo de la apo</string> + <string name="pref_title_timelines">Tempolinioj</string> + <string name="pref_title_timeline_filters">Filtriloj</string> + <string name="app_them_dark">Malhela</string> + <string name="app_theme_light">Hela</string> + <string name="app_theme_black">Nigra</string> + <string name="app_theme_auto">Aŭtomata laŭ la horo</string> + <string name="app_theme_system">Uzi sisteman etoson</string> + <string name="pref_title_browser_settings">Retumilo</string> + <string name="pref_title_custom_tabs">Uzi la integritan retumilon</string> + <string name="pref_title_language">Lingvo</string> + <string name="pref_title_post_filter">Filtrado de tempolinioj</string> + <string name="pref_title_post_tabs">Langetoj</string> + <string name="pref_title_show_boosts">Montri diskonigojn</string> + <string name="pref_title_show_replies">Montri la respondojn</string> + <string name="pref_title_show_media_preview">Elŝuti antaŭvidojn de aŭdovidaĵoj</string> + <string name="pref_title_proxy_settings">Prokurilo</string> + <string name="pref_title_http_proxy_settings">HTTP-prokurilo</string> + <string name="pref_title_http_proxy_enable">Ebligi HTTP-prokurilon</string> + <string name="pref_title_http_proxy_server">Adreso de HTTP-prokurilo</string> + <string name="pref_title_http_proxy_port">Pordo de HTTP-prokurilo</string> + <string name="pref_default_post_privacy">Dekomenca privateco de mesaĝoj</string> + <string name="pref_default_media_sensitivity">Ĉiam marki aŭdovidaĵojn kiel tiklajn</string> + <string name="pref_publishing">Publikigante (sinkronigita kun la servilo)</string> + <string name="pref_failed_to_sync">Sinkronigo de la agordoj malsukcesis</string> + <string name="post_privacy_public">Publika</string> + <string name="post_privacy_unlisted">Nelistigita</string> + <string name="post_privacy_followers_only">Nur por sekvantoj</string> + <string name="pref_post_text_size">Grando de teksto de mesaĝoj</string> + <string name="post_text_size_smallest">La plej malgranda</string> + <string name="post_text_size_small">Malgranda</string> + <string name="post_text_size_medium">Meza</string> + <string name="post_text_size_large">Granda</string> + <string name="post_text_size_largest">La plej granda</string> + <string name="notification_mention_name">Novaj mencioj</string> + <string name="notification_mention_descriptions">Sciigoj pri novaj mencioj</string> + <string name="notification_follow_name">Novaj sekvantoj</string> + <string name="notification_follow_description">Sciigoj pri novaj sekvantoj</string> + <string name="notification_boost_name">Diskonigoj</string> + <string name="notification_boost_description">Sciigoj, kiam viaj mesaĝoj estas diskonigitaj</string> + <string name="notification_favourite_name">Stelumoj</string> + <string name="notification_favourite_description">Sciigoj, kiam viaj mesaĝoj estas stelumitaj</string> + <string name="notification_mention_format">%1$s menciis vin</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s kaj %4$d aliaj</string> + <string name="notification_summary_medium">%1$s, %2$s kaj %3$s</string> + <string name="notification_summary_small">%1$s kaj %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d nova interago</item> + <item quantity="other">%1$d novaj interagoj</item> + </plurals> + <string name="description_account_locked">Ŝlosita konto</string> + <string name="about_title_activity">Pri</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tusky estas libera kaj malfermitkoda programo. + Ĝi estas publikigita laŭ la permesilo «GNU General Public License Version 3». + Vi povas vidi la permesilon ĉi tie: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">Paĝaro de la projekto: +\n https://tusky.app</string> + <string name="about_bug_feature_request_site">Raportoj de cimoj kaj petoj de funkcioj: +\n https://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Profilo de Tusky</string> + <string name="post_share_content">Konigi enhavon de la mesaĝo</string> + <string name="post_share_link">Konigi ligilon al mesaĝo</string> + <string name="post_media_images">Bildoj</string> + <string name="post_media_video">Video</string> + <string name="state_follow_requested">Sekvado petita</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">post %1$dj</string> + <string name="abbreviated_in_days">post %1$dt</string> + <string name="abbreviated_in_hours">post %1$dh</string> + <string name="abbreviated_in_minutes">post %1$dm</string> + <string name="abbreviated_in_seconds">post %1$ds</string> + <string name="abbreviated_years_ago">%1$dj</string> + <string name="abbreviated_days_ago">%1$dt</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="follows_you">Sekvas vin</string> + <string name="pref_title_alway_show_sensitive_media">Ĉiam montri tiklan enhavon</string> + <string name="title_media">Aŭdovidaĵoj</string> + <string name="replying_to">Respondo al @%1$s</string> + <string name="load_more_placeholder_text">ŝarĝi pli</string> + <string name="pref_title_public_filter_keywords">Publikaj tempolinioj</string> + <string name="pref_title_thread_filter_keywords">Konversacioj</string> + <string name="filter_addition_title">Aldoni filtrilon</string> + <string name="filter_edit_title">Redakti filtrilon</string> + <string name="filter_dialog_remove_button">Forigi</string> + <string name="filter_dialog_update_button">Aktualigi</string> + <string name="filter_add_description">Frazo filtrota</string> + <string name="add_account_name">Aldoni konton</string> + <string name="add_account_description">Aldoni novan Mastodon-konton</string> + <string name="action_lists">Listoj</string> + <string name="title_lists">Listoj</string> + <string name="error_create_list">Ne povis krei la liston</string> + <string name="error_rename_list">Ne povis ŝanĝi la nomon de la listo</string> + <string name="error_delete_list">Ne povis forigi la liston</string> + <string name="action_create_list">Krei liston</string> + <string name="action_rename_list">Ŝanĝi la nomon de la listo</string> + <string name="action_delete_list">Forigi la liston</string> + <string name="hint_search_people_list">Serĉi homojn, kiujn vi sekvas</string> + <string name="action_add_to_list">Aldoni konton al la listo</string> + <string name="action_remove_from_list">Forigi konton el la listo</string> + <string name="compose_active_account_description">Afiŝi per konto %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one"/> + <item quantity="other">Priskribi por vide handikapitaj homoj +\n(%1$d signoj maksimume)</item> + </plurals> + <string name="action_set_caption">Redakti apudskribon</string> + <string name="action_remove">Forigi</string> + <string name="lock_account_label">Ŝlosi konton</string> + <string name="lock_account_label_description">Vi devas permane rajtigi sekvantojn</string> + <string name="compose_save_draft">Ĉu konservi malneton\?</string> + <string name="send_post_notification_title">Sendado de la mesaĝo…</string> + <string name="send_post_notification_error_title">Eraro dum sendo de la mesaĝo</string> + <string name="send_post_notification_channel_name">Sendado de la mesaĝoj</string> + <string name="send_post_notification_cancel_title">Sendo nuligita</string> + <string name="send_post_notification_saved_content">Kopio de la mesaĝo estis konservita en viaj malnetoj</string> + <string name="action_compose_shortcut">Verki</string> + <string name="error_no_custom_emojis">Via nodo %1$s ne havas proprajn emoĝiojn</string> + <string name="emoji_style">Stilo de emoĝioj</string> + <string name="system_default">El la sistemo</string> + <string name="download_fonts">Vi unue devos elŝuti ĉi tiun emoĝiaron</string> + <string name="performing_lookup_title">Serĉado…</string> + <string name="expand_collapse_all_posts">Pligrandigi/malgrandigi ĉiujn mesaĝojn</string> + <string name="action_open_post">Malfermi mesaĝon</string> + <string name="restart_required">Restartigo necesas</string> + <string name="restart_emoji">Vi devos restartigi Tusky por apliki ĉi tiujn ŝanĝojn</string> + <string name="later">Poste</string> + <string name="restart">Restartigi</string> + <string name="caption_systememoji">Dekomenca emoĝiaro de via aparato</string> + <string name="caption_blobmoji">La emoĝioj «Blob» konataj de Android 4.4−7.1</string> + <string name="caption_twemoji">Norma emoĝiaro de Mastodon</string> + <string name="download_failed">Elŝuto malsukcesis</string> + <string name="profile_badge_bot_text">Roboto</string> + <string name="account_moved_description">%1$s moviĝis al:</string> + <string name="reblog_private">Diskonigi al la originala atentaro</string> + <string name="unreblog_private">Maldiskonigi</string> + <string name="license_description">Tusky enhavas kodon kaj risurcojn el la sekvantaj malfermitkodaj projetkoj:</string> + <string name="license_apache_2">Laŭ la permesilo «Apache» (kopio sube)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Profilaj metadatumoj</string> + <string name="profile_metadata_add">aldoni datumon</string> + <string name="profile_metadata_label_label">Etikedo</string> + <string name="profile_metadata_content_label">Enhavo</string> + <string name="pref_title_absolute_time">Uzi absolutan tempon</string> + <string name="label_remote_account">Subaj informoj povas nekomplete prezenti la profilon de la uzanto. Tuŝi por malfermi la kompletan profilon en retumilo.</string> + <string name="unpin_action">Depingli</string> + <string name="pin_action">Alpingli</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Stelumo</item> + <item quantity="other"><b>%1$s</b> Stelumoj</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> Diskonigo</item> + <item quantity="other"><b>%1$s</b> Diskonigoj</item> + </plurals> + <string name="title_reblogged_by">Diskonigita de</string> + <string name="title_favourited_by">Stelumita de</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s kaj %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s kaj %3$d aliaj</string> + <string name="description_post_media"> Aŭdovidaĵo: %1$s + </string> + <string name="description_post_cw"> Enhava averto: %1$s + </string> + <string name="description_post_media_no_description_placeholder"> Neniu priskribo + </string> + <string name="description_post_reblogged"> Diskonigita + </string> + <string name="description_post_favourited"> Stelumita + </string> + <string name="description_visibility_public"> Publika + </string> + <string name="description_visibility_unlisted"> Nelistigita </string> + <string name="description_visibility_private"> Sekvantoj + </string> + <string name="description_visibility_direct"> Rekta </string> + <string name="hint_list_name">Nomo de la listo</string> + <string name="action_delete_and_redraft">Forigi kaj reskribi</string> + <string name="dialog_redraft_post_warning">Ĉu forigi kaj reskribi ĉi tiun mesaĝon\?</string> + <string name="pref_title_notification_filter_poll">enketoj finiĝis</string> + <string name="pref_title_bot_overlay">Montri indikilon pri robotoj</string> + <string name="pref_title_animate_gif_avatars">Ebligi GIF-profilbildojn</string> + <string name="notification_poll_name">Enketoj</string> + <string name="notification_poll_description">Sciigoj pri enketoj, kiuj finiĝis</string> + <string name="edit_hashtag_hint">Kradvorto sen #</string> + <string name="notifications_clear">Viŝi</string> + <string name="notifications_apply_filter">Filtri</string> + <string name="filter_apply">Apliki</string> + <string name="compose_shortcut_long_label">Verki mesaĝon</string> + <string name="compose_shortcut_short_label">Verki</string> + <string name="notification_clear_text">Ĉu vi certas, ke vi volas porĉiame viŝi ĉiujn viajn sciigojn\?</string> + <string name="compose_preview_image_description">Agoj por bildo %1$s</string> + <string name="poll_info_format"> <!-- 15 votes • 1 hour left --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s voĉdono</item> + <item quantity="other">%1$s voĉdonoj</item> + </plurals> + <string name="poll_info_time_absolute">finiĝos je %1$s</string> + <string name="poll_info_closed">finita</string> + <string name="poll_vote">Voĉdoni</string> + <string name="poll_ended_voted">Enketo, al kiu vi voĉdonis, finiĝis</string> + <string name="poll_ended_created">Enketo, kiun vi kreis, finiĝis</string> + <string name="title_domain_mutes">Kaŝitaj domajnoj</string> + <string name="action_view_domain_mutes">Kaŝitaj domajnoj</string> + <string name="action_mute_domain">Silentigi %1$s</string> + <string name="confirmation_domain_unmuted">%1$s malsilentigita</string> + <string name="mute_domain_warning">Ĉu vi certas ke vi volas tute bloki %1$s\? Vi ne vidos enhavon de tiu domajno en publika tempolinio aŭ en viaj sciigoj. Viaj sekvantoj el tiu domajno estos forigitaj.</string> + <string name="mute_domain_warning_dialog_ok">Kaŝi la tutan domajnon</string> + <string name="caption_notoemoji">La aktuala emoĝiaro de Google</string> + <string name="description_poll">Balotenketo kun elektoj: %1$s, %2$s, %3$s, %4$s, %5$s</string> + <string name="button_continue">Daŭrigi</string> + <string name="button_back">Reveni</string> + <string name="button_done">Farita</string> + <string name="report_sent_success">\@%1$s sukcese signalita</string> + <string name="hint_additional_info">Pliaj komentoj</string> + <string name="report_remote_instance">Plusendi al %1$s</string> + <string name="failed_report">Signalo malsukcesis</string> + <string name="failed_fetch_posts">Venigo de statusoj malsukcesis</string> + <string name="report_description_1">La signalo estos sendita al la kontrolantoj de via servilo. Vi povas doni klarigon pri kial vi signalas ĉi tiun konton sube:</string> + <string name="report_description_remote_instance">La konto estas en alia servilo. Ĉu sendi sennomigitan kopion de la signalo ankaŭ tien\?</string> + <string name="filter_dialog_whole_word">Tuta vorto</string> + <string name="filter_dialog_whole_word_description">Ŝlosilvorto aŭ frazo litercifera aplikiĝos, nur se ĝi kongruas kun la tuta vorto</string> + <string name="title_accounts">Kontoj</string> + <string name="failed_search">Serĉo malsukcesis</string> + <string name="action_add_poll">Aldoni baloton</string> + <string name="pref_title_alway_open_spoiler">Ĉiam montri mesaĝojn kun enhavaj avertoj</string> + <string name="create_poll_title">Enketo</string> + <string name="duration_5_min">5 minutoj</string> + <string name="duration_30_min">30 minutoj</string> + <string name="duration_1_hour">1 horo</string> + <string name="duration_6_hours">6 horoj</string> + <string name="duration_1_day">1 tago</string> + <string name="duration_3_days">3 tagoj</string> + <string name="duration_7_days">7 tagoj</string> + <string name="add_poll_choice">Aldoni elekton</string> + <string name="poll_allow_multiple_choices">Multaj elektoj</string> + <string name="poll_new_choice_hint">Elekton %1$d</string> + <string name="edit_poll">Redakti</string> + <string name="title_bookmarks">Legosignoj</string> + <string name="title_scheduled_posts">Planitaj mesaĝoj</string> + <string name="action_bookmark">Aldoni al la legosignoj</string> + <string name="action_edit">Redakti</string> + <string name="action_view_bookmarks">Legosignoj</string> + <string name="action_access_scheduled_posts">Planitaj mesaĝoj</string> + <string name="action_schedule_post">Plani mesaĝon</string> + <string name="action_reset_schedule">Restarigi</string> + <string name="about_powered_by_tusky">Funkciigita de Tusky</string> + <string name="description_post_bookmarked">Aldonita al la legosignoj</string> + <string name="select_list_title">Elekti la liston</string> + <string name="list">Listo</string> + <string name="post_lookup_error_format">Eraro dum serĉo de la mesaĝo %1$s</string> + <string name="no_drafts">Vi havas neniun malneton.</string> + <string name="no_scheduled_posts">Vi havas neniun planitan mesaĝon.</string> + <string name="notification_follow_request_name">Petoj de sekvado</string> + <string name="hashtags">Kradvortoj</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s homo</item> + <item quantity="other">%1$s homoj</item> + </plurals> + <string name="add_hashtag_title">Aldoni kradvorton</string> + <string name="notification_follow_request_description">Sciigoj pri petoj de sekvado</string> + <string name="pref_title_gradient_for_media">Montri buntajn transirojn por kaŝitaj aŭdovidaĵoj</string> + <string name="dialog_mute_hide_notifications">Kaŝi la sciigojn</string> + <string name="dialog_mute_warning">Ĉu silentigi @%1$s\?</string> + <string name="dialog_block_warning">Ĉu bloki @%1$s\?</string> + <string name="action_unmute_conversation">Malsilentigi la konversacion</string> + <string name="action_mute_conversation">Silentigi la konversacion</string> + <string name="action_unmute_domain">Malsilentigi %1$s</string> + <string name="action_unmute_desc">Malsilentigi %1$s</string> + <string name="notification_follow_request_format">%1$s petis sekvi vin</string> + <string name="title_announcements">Anoncoj</string> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d minuto restas</item> + <item quantity="other">%1$d minutoj restas</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d sekundo restas</item> + <item quantity="other">%1$d sekundoj restas</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d horo restas</item> + <item quantity="other">%1$d horoj restas</item> + </plurals> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d tago restas</item> + <item quantity="other">%1$d tagoj restas</item> + </plurals> + <string name="pref_main_nav_position">Pozicio de la ĉefa naviga breto</string> + <string name="pref_title_notification_filter_follow_requests">iu petas sekvi min</string> + <string name="pref_main_nav_position_option_bottom">Malsupro</string> + <string name="pref_main_nav_position_option_top">Supro</string> + <string name="account_note_saved">Konservita!</string> + <string name="account_note_hint">Via privata noto pri ĉi tiu konto</string> + <string name="pref_title_hide_top_toolbar">Kaŝi la titolon de la supra ilobreto</string> + <string name="pref_title_show_cards_in_timelines">Montri antaŭvidojn de ligiloj en tempolinioj</string> + <string name="pref_title_confirm_reblogs">Montri konfirman fenestron antaŭ ol diskonigi</string> + <string name="no_announcements">Estas neniu anonco.</string> + <string name="pref_title_enable_swipe_for_tabs">Ebligi ŝovumadon por ŝanĝi inter la langetoj</string> + <string name="warning_scheduling_interval">Mastodon havas minimuman intervalon de planado de 5 minutoj.</string> + <string name="post_media_attachments">Kunsendaĵoj</string> + <string name="pref_title_notification_filter_subscriptions">iu, kiun mi sekvas, afiŝis novan mesaĝon</string> + <string name="dialog_delete_list_warning">Ĉu vi vere volas forigi la liston %1$s\?</string> + <string name="post_media_audio">Aŭdaĵo</string> + <string name="action_subscribe_account">Aboni</string> + <string name="draft_deleted">Malneto forigita</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Vi ne povas elŝuti pli ol %1$d aŭdovida kunsendaĵo.</item> + <item quantity="other">Vi ne povas elŝuti pli ol %1$d aŭdovidaj kunsendaĵoj.</item> + </plurals> + <string name="label_duration">Daŭro</string> + <string name="duration_indefinite">Nedefinita</string> + <string name="action_unsubscribe_account">Malaboni</string> + <string name="notification_subscription_name">Novaj mesaĝoj</string> + <string name="action_unbookmark">Forigi la legosignon</string> + <string name="dialog_delete_conversation_warning">Ĉu forigi ĉi tiun konversacion\?</string> + <string name="pref_title_animate_custom_emojis">Animacii proprajn emoĝiojn</string> + <string name="wellbeing_hide_stats_profile">Kaŝi kvantecajn statistikaĵojn pri la profiloj</string> + <string name="action_delete_conversation">Forigi konversacion</string> + <string name="notification_subscription_format">%1$s ĵus afiŝis</string> + <string name="notification_subscription_description">Sciigoj, kiam iu, kiun vi sekvas, afiŝis novan mesaĝon</string> + <string name="drafts_post_failed_to_send">Sendo de ĉi tiu mesaĝo malsukcesis!</string> + <string name="wellbeing_hide_stats_posts">Kaŝi kvantecajn statistikaĵojn pri la mesaĝoj</string> + <string name="pref_title_confirm_favourites">Demandi konfirmon antaŭ ol stelumi</string> + <string name="pref_title_wellbeing_mode">Bonstato</string> + <string name="drafts_failed_loading_reply">Ŝarĝado de respondaj informoj malsukcesis</string> + <string name="wellbeing_mode_notice">Kelkaj informoj kiuj povas afekci vian mensan bonstaton estos kaŝitaj. Ĉi tiuj inkluzivas: +\n +\n — Sciigoj pri stelumo/diskonigo/sekvado +\n — Nombro de stelumoj/diskonigoj sur la mesaĝoj +\n — Statistikoj pri mesaĝoj/sekvantoj sur la profiloj +\n +\n Sciigoj ne estos influitaj, sed vi povas kontroli viajn agordojn pri sciigojn permane.</string> + <string name="review_notifications">Kontroli la sciigojn</string> + <string name="limit_notifications">Limigi sciigojn pri tempolinio</string> + <string name="drafts_post_reply_removed">La mesaĝo, al kiu tiu ĉi malneto respondas, estis forigita</string> + <string name="notification_sign_up_format">%1$s registriĝis</string> + <string name="pref_title_notification_filter_sign_ups">iu registriĝis</string> + <string name="pref_title_notification_filter_updates">mesaĝo, kun kiu mi interagis, estas redaktita</string> + <string name="notification_sign_up_name">Novaj kontoj</string> + <string name="notification_sign_up_description">Sciigoj pri novaj uzantoj</string> + <string name="status_count_one_plus">1+</string> + <string name="follow_requests_info">Kvankam via konto ne estas ŝlosita, la teamo de %1$s pensas, ke vi eble volus permane validigi la sekvopetojn de tiuj ĉi kontoj.</string> + <string name="error_multimedia_size_limit">Filmetoj kaj sondosieroj ne povas esti pli grandaj ol %1$s MB.</string> + <string name="error_image_edit_failed">La bildo ne povis esti redaktita.</string> + <string name="title_login">Ensaluti</string> + <string name="error_following_hashtag_format">Okazis eraro dum la sekvado de #%1$s</string> + <string name="error_unfollowing_hashtag_format">Okazos eraro dum la malsekvado de #%1$s</string> + <string name="title_migration_relogin">Ensaluti denove por ricevi sciigojn</string> + <string name="notification_update_format">%1$s redaktis sian mesaĝon</string> + <string name="action_dismiss">Fermi</string> + <string name="action_details">Detaloj</string> + <string name="notification_update_name">Redaktitaj mesaĝoj</string> + <string name="notification_update_description">Sciigoj, kiam mesaĝoj, kun kiuj vi interagis, estas redaktitaj</string> + <string name="action_edit_image">Redakti la bildon</string> + <string name="duration_14_days">14 tagoj</string> + <string name="duration_30_days">30 tagoj</string> + <string name="duration_60_days">60 tagoj</string> + <string name="duration_90_days">90 tagoj</string> + <string name="duration_180_days">180 tagoj</string> + <string name="duration_365_days">365 tagoj</string> + <string name="tusky_compose_post_quicksetting_label">Ekverki mesaĝon</string> + <string name="account_date_joined">Aliĝis je %1$s</string> + <string name="saving_draft">Konservado de la malneto…</string> + <string name="tips_push_notification_migration">Ensalutu denove al ĉiuj kontoj por ŝalti sciigojn.</string> + <string name="error_could_not_load_login_page">La salutpaĝo ne povis esti ŝargita.</string> + <string name="error_loading_account_details">Ŝargo de detaloj pri la konto malsukcesis</string> + <string name="delete_scheduled_post_warning">Ĉu forigi tiun planitan mesaĝon\?</string> + <string name="instance_rule_info">Si vi ensalutas, vi konsentas je la regulo de %1$s.</string> + <string name="instance_rule_title">Regulo de %1$s</string> + <string name="duration_no_change">(Neniu ŝanĝo)</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="pref_show_self_username_always">Ĉiam</string> + <string name="pref_show_self_username_disambiguate">Kiam vi uzas plurajn kontojn</string> + <string name="pref_show_self_username_never">Neniam</string> + <string name="pref_title_show_self_username">Montri uzantnomon en ilobreto</string> + <string name="description_post_language">Mesaĝolingvo</string> + <string name="dialog_push_notification_migration">Por ricevi sciigoj per UnifiedPush, Tusky bezonas taŭgan permeson el Mastodon-servilo. Tio postulas re-ensaluton por ŝanĝi OAuth-rajtoj donitaj al Tusky. Se vi uzas la opcion re-ensaluti ĉi tie aŭ en la agordoj de la konto, viaj malnetoj kaj kaŝmemoroj estos konservitaj.</string> + <string name="dialog_push_notification_migration_other_accounts">Vi re-ensalutis en tiu konto por doni sciigo-permeson al Tusky. Vi havas tamen aliajn kontojn, ĉe kiuj vi devas re-sensaluti. Iru al ili, kaj re-ensalutu por ebligi ricevon de sciigoj per UnifiedPush.</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml new file mode 100644 index 0000000..6313cc8 --- /dev/null +++ b/app/src/main/res/values-es/strings.xml @@ -0,0 +1,678 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Ha ocurrido un error.</string> + <string name="error_network">Se ha producido un error de red. Por favor, comprueba tu conexión e inténtalo de nuevo.</string> + <string name="error_empty">Este campo no puede estar vacío.</string> + <string name="error_invalid_domain">Nombre de dominio incorrecto</string> + <string name="error_failed_app_registration">Fallo de autenticación con esta instancia. Si esto persiste, prueba en el menú Iniciar sesión con el navegador.</string> + <string name="error_no_web_browser_found">No se ha encontrado ningún navegador web.</string> + <string name="error_authorization_unknown">Ocurrió un error de autorización no identificado. Si esto persiste, prueba en el menú Iniciar sesión con el navegador.</string> + <string name="error_authorization_denied">La autorización falló. Si estás seguro de que has suministrado las credenciales correctas, prueba en el menú Iniciar sesión con el navegador.</string> + <string name="error_retrieving_oauth_token">Fallo al obtener identificador de login. Si esto persiste, prueba en el menú Iniciar sesión con el navegador.</string> + <string name="error_compose_character_limit">¡La publicación es demasiado larga!</string> + <string name="error_media_upload_type">No se admite este tipo de archivo.</string> + <string name="error_media_upload_opening">No pudo abrirse el fichero.</string> + <string name="error_media_upload_permission">Se requiere permiso para acceder al almacenamiento.</string> + <string name="error_media_download_permission">Se requiere permiso para descargar al almacenamiento.</string> + <string name="error_media_upload_image_or_video">No se pueden adjuntar imágenes y vídeos en la misma publicación.</string> + <string name="error_media_upload_sending">La subida falló.</string> + <string name="error_sender_account_gone">Error al publicar.</string> + <string name="title_home">Inicio</string> + <string name="title_notifications">Notificaciones</string> + <string name="title_public_local">Local</string> + <string name="title_public_federated">Federada</string> + <string name="title_direct_messages">Mensajes directos</string> + <string name="title_tab_preferences">Pestañas</string> + <string name="title_view_thread">Hilo</string> + <string name="title_posts">Estados</string> + <string name="title_posts_with_replies">Con respuestas</string> + <string name="title_posts_pinned">Fijado</string> + <string name="title_follows">Siguiendo</string> + <string name="title_followers">Seguidores</string> + <string name="title_favourites">Favoritos</string> + <string name="title_mutes">Silenciados</string> + <string name="title_blocks">Bloqueados</string> + <string name="title_follow_requests">Solicitudes</string> + <string name="title_edit_profile">Editar tu perfil</string> + <string name="title_drafts">Borradores</string> + <string name="title_licenses">Licencias</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s compartió</string> + <string name="post_sensitive_media_title">Contenido sensible</string> + <string name="post_media_hidden_title">Material oculto</string> + <string name="post_sensitive_media_directions">Pulsa para ver</string> + <string name="post_content_warning_show_more">Mostrar más</string> + <string name="post_content_warning_show_less">Mostrar menos</string> + <string name="post_content_show_more">Expandir</string> + <string name="post_content_show_less">Ocultar</string> + <string name="message_empty">Nada por aquí.</string> + <string name="footer_empty">Nada por aquí. ¡Arrastra hacia abajo para recargar!</string> + <string name="notification_reblog_format">%1$s impulsó tu publicación</string> + <string name="notification_favourite_format">%1$s marcó como favorita tu publicación</string> + <string name="notification_follow_format">%1$s te siguió</string> + <string name="report_username_format">Reportar @%1$s</string> + <string name="report_comment_hint">¿Información adicional?</string> + <string name="action_quick_reply">Respuesta rápida</string> + <string name="action_reply">Responder</string> + <string name="action_reblog">Compartir</string> + <string name="action_favourite">Favorito</string> + <string name="action_more">Más</string> + <string name="action_compose">Redactar</string> + <string name="action_login">Iniciar sesión con Tusky</string> + <string name="action_logout">Cerrar sesión</string> + <string name="action_logout_confirm">¿Seguro que quiere cerrar la sesión de %1$s?</string> + <string name="action_follow">Seguir</string> + <string name="action_unfollow">Dejar de seguir</string> + <string name="action_block">Bloquear</string> + <string name="action_unblock">Desbloquear</string> + <string name="action_hide_reblogs">Ocultar impulsos</string> + <string name="action_show_reblogs">Mostrar impulsos</string> + <string name="action_report">Reportar</string> + <string name="action_delete">Borrar</string> + <string name="action_send">TOOT</string> + <string name="action_send_public">¡Publicar!</string> + <string name="action_retry">Reintentar</string> + <string name="action_close">Cerrar</string> + <string name="action_view_profile">Perfil</string> + <string name="action_view_preferences">Ajustes</string> + <string name="action_view_account_preferences">Preferencias de cuenta</string> + <string name="action_view_favourites">Favoritos</string> + <string name="action_view_mutes">Silenciados</string> + <string name="action_view_blocks">Bloqueados</string> + <string name="action_view_follow_requests">Solicitudes</string> + <string name="action_view_media">Multimedia</string> + <string name="action_open_in_web">Abrir en el navegador</string> + <string name="action_add_media">Añadir multimedia</string> + <string name="action_photo_take">Tomar foto</string> + <string name="action_share">Compartir</string> + <string name="action_mute">Silenciar</string> + <string name="action_unmute">Dejar de silenciar</string> + <string name="action_mention">Mencionar</string> + <string name="action_hide_media">Ocultar multimedia</string> + <string name="action_open_drawer">Abrir menu</string> + <string name="action_save">Guardar</string> + <string name="action_edit_profile">Editar perfil</string> + <string name="action_edit_own_profile">Editar</string> + <string name="action_undo">Deshacer</string> + <string name="action_accept">Aceptar</string> + <string name="action_reject">Rechazar</string> + <string name="action_search">Buscar</string> + <string name="action_access_drafts">Borradores</string> + <string name="action_toggle_visibility">Visibilidad de la publicación</string> + <string name="action_content_warning">Aviso de contenido</string> + <string name="action_emoji_keyboard">Teclado de emojis</string> + <string name="action_add_tab">Añadir pestaña</string> + <string name="download_image">Descargando %1$s</string> + <string name="action_copy_link">Copiar el enlace</string> + <string name="action_open_as">Abrir como %1$s</string> + <string name="action_share_as">Compartir como…</string> + <string name="send_post_link_to">Compartir URL…</string> + <string name="send_post_content_to">Compartir contenido…</string> + <string name="send_media_to">Compartir medios a…</string> + <string name="confirmation_reported">¡Enviado!</string> + <string name="confirmation_unblocked">El usuario ya no está bloqueado</string> + <string name="confirmation_unmuted">El usuario ya no está silenciado</string> + <string name="hint_domain">¿Qué instancia\?</string> + <string name="hint_compose">¿En qué estás pensando?</string> + <string name="hint_content_warning">Aviso de contenido</string> + <string name="hint_display_name">Nombre</string> + <string name="hint_note">Biografía</string> + <string name="hint_search">Buscar…</string> + <string name="search_no_results">Sin resultados</string> + <string name="label_quick_reply">Respuesta…</string> + <string name="label_avatar">Avatar</string> + <string name="label_header">Cabecera</string> + <string name="link_whats_an_instance">¿Qué es una instancia?</string> + <string name="login_connection">Conectando…</string> + <string name="dialog_whats_an_instance">Introduzca aquí dirección o dominio de cualquier instancia, + como mastodon.social, icosahedron.website, social.tchncs.de y + <a href="https://instances.social">más!</a> + \n\nSi todavia no tiene una cuenta, puede indicar el nombre de la instancia a la que quiere + unirse y crear una cuenta allí.\n\nUna instancia es el sitio único donde su cuenta + está alojada, pero puede comunicarse y seguir usuarios de otras instancias como si + estuvieran en la misma. + \n\nPuede consultar más información en <a href="https://joinmastodon.org">joinmastodon.org</a>. + </string> + <string name="dialog_title_finishing_media_upload">Terminando de subir multimedia</string> + <string name="dialog_message_uploading_media">Subiendo…</string> + <string name="dialog_download_image">Descargar</string> + <string name="dialog_message_cancel_follow_request">¿Cancelar petición de amistad?</string> + <string name="dialog_unfollow_warning">¿Dejar de seguir esta cuenta?</string> + <string name="dialog_delete_post_warning">¿Eliminar este toot\?</string> + <string name="visibility_public">Público: Mostrar en cronologías públicas</string> + <string name="visibility_unlisted">Oculto: No mostrar en cronologías públicas</string> + <string name="visibility_private">Privado: Sólo visible para seguidores</string> + <string name="visibility_direct">Directo: Sólo visible para cuentas mencionadas</string> + <string name="pref_title_edit_notification_settings">Notificaciones</string> + <string name="pref_title_notifications_enabled">Editar notificaciones</string> + <string name="pref_title_notification_alerts">Alertas</string> + <string name="pref_title_notification_alert_sound">Notificar con sonido</string> + <string name="pref_title_notification_alert_vibrate">Notificar con vibración</string> + <string name="pref_title_notification_alert_light">Notificar con led</string> + <string name="pref_title_notification_filters">Notificar cuando</string> + <string name="pref_title_notification_filter_mentions">me mencionan</string> + <string name="pref_title_notification_filter_follows">me siguen</string> + <string name="pref_title_notification_filter_reblogs">mis posts son impulsados</string> + <string name="pref_title_notification_filter_favourites">me dan favorito</string> + <string name="pref_title_appearance_settings">Apariencia</string> + <string name="pref_title_app_theme">Tema de la app</string> + <string name="pref_title_timelines">Cronología</string> + <string name="app_them_dark">Oscuro</string> + <string name="app_theme_light">Claro</string> + <string name="app_theme_black">Negro</string> + <string name="app_theme_auto">Automático</string> + <string name="pref_title_browser_settings">Navegador</string> + <string name="pref_title_custom_tabs">Usar Chrome Custom Tabs</string> + <string name="pref_title_post_filter">Filtros de cronología</string> + <string name="pref_title_post_tabs">Pestañas</string> + <string name="pref_title_show_boosts">Mostrar impulsos</string> + <string name="pref_title_show_replies">Mostrar respuestas</string> + <string name="pref_title_show_media_preview">Previsualizar multimedia</string> + <string name="pref_title_proxy_settings">Proxy</string> + <string name="pref_title_http_proxy_settings">Proxy HTTP</string> + <string name="pref_title_http_proxy_enable">Habilitar proxy HTTP</string> + <string name="pref_title_http_proxy_server">Servidor del proxy HTTP</string> + <string name="pref_title_http_proxy_port">Puerto del proxy HTTP</string> + <string name="pref_default_post_privacy">Visibilidad por defecto</string> + <string name="pref_default_media_sensitivity">Marcar siempre medios como sensibles</string> + <string name="pref_publishing">Publicaciones</string> + <string name="pref_failed_to_sync">Error al sincronizar los ajustes</string> + <string name="post_privacy_public">Público</string> + <string name="post_privacy_unlisted">Oculto</string> + <string name="post_privacy_followers_only">Privado</string> + <string name="pref_post_text_size">Tamaño del texto</string> + <string name="post_text_size_smallest">Diminuto</string> + <string name="post_text_size_small">Pequeño</string> + <string name="post_text_size_medium">Medio</string> + <string name="post_text_size_large">Grande</string> + <string name="post_text_size_largest">Enorme</string> + <string name="notification_mention_name">Nuevas menciones</string> + <string name="notification_mention_descriptions">Notificaciones de nuevas menciones</string> + <string name="notification_follow_name">Nuevos seguidores</string> + <string name="notification_follow_description">Notificaciones de nuevos seguidores</string> + <string name="notification_boost_name">Impulsos</string> + <string name="notification_boost_description">Notificaciones cuando impulsan tus publicaciones</string> + <string name="notification_favourite_name">Favoritos</string> + <string name="notification_favourite_description">Notificaciones de tus estados marcados como favorito</string> + <string name="notification_mention_format">%1$s te mencionó</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s y %4$d otros</string> + <string name="notification_summary_medium">%1$s, %2$s, y %3$s</string> + <string name="notification_summary_small">%1$s y %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d nueva interacción</item> + <item quantity="many">%1$d nuevas interacciones</item> + <item quantity="other">%1$d nuevas interacciones</item> + </plurals> + <string name="description_account_locked">Cuenta protegida</string> + <string name="about_title_activity">Acerca de</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tusky es un software libre y de código abierto. + Está licenciado bajo la licencia \"GNU General Public License Version 3\". + Puedes leer sobre la misma en: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site"> Sitio del proyecto:\n + https://tusky.app + </string> + <string name="about_bug_feature_request_site"> Reporte de errores y peticiones de características:\n + https://github.com/tuskyapp/Tusky/issues + </string> + <string name="about_tusky_account">Perfil de Tusky</string> + <string name="post_share_content">Compartir contenido</string> + <string name="post_share_link">Compartir enlace</string> + <string name="post_media_images">Imágenes</string> + <string name="post_media_video">Video</string> + <string name="state_follow_requested">Solicitud enviada</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="status_created_at_now">Ahora</string> + <string name="abbreviated_in_years">en %1$dy</string> + <string name="abbreviated_in_days">en %1$dd</string> + <string name="abbreviated_in_hours">en %1$dh</string> + <string name="abbreviated_in_minutes">en %1$dm</string> + <string name="abbreviated_in_seconds">en %1$ds</string> + <string name="abbreviated_years_ago">%1$dy</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="follows_you">Te sigue</string> + <string name="pref_title_alway_show_sensitive_media">Mostrar contenido NSFW</string> + <string name="title_media">Multimedia</string> + <string name="replying_to">Respondiendo a @%1$s</string> + <string name="load_more_placeholder_text">cargar más</string> + <string name="add_account_name">Añadir cuenta</string> + <string name="add_account_description">Añadir cuenta de Mastodon</string> + <string name="action_lists">Listas</string> + <string name="title_lists">Listas</string> + <string name="compose_active_account_description">Publicar como %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Descripción para personas con problemas de visión +\n(Límite de %1$d caracter)</item> + <item quantity="many">Descripción para personas con problemas de visión +\n(Límite de %1$d caracteres)</item> + <item quantity="other">Descripción para personas con problemas de visión +\n(Límite de %1$d caracteres)</item> + </plurals> + <string name="action_set_caption">Añadir leyenda</string> + <string name="action_remove">Eliminar</string> + <string name="lock_account_label">Proteger cuenta</string> + <string name="lock_account_label_description">Tendrá que admitir los seguidores manualmente</string> + <string name="compose_save_draft">¿Guardar borrador?</string> + <string name="send_post_notification_title">Enviando estado…</string> + <string name="send_post_notification_error_title">Error al enviar el estado</string> + <string name="send_post_notification_channel_name">Enviando estado</string> + <string name="send_post_notification_cancel_title">Envío cancelado</string> + <string name="send_post_notification_saved_content">Una copia del estado se ha guardado en borradores</string> + <string name="action_compose_shortcut">Redactar</string> + <string name="error_no_custom_emojis">Su instancia %1$s no ofrece emojis personalizados</string> + <string name="emoji_style">Estilo de los emojis</string> + <string name="system_default">Sistema</string> + <string name="download_fonts">Tendrás que descargarlos primero</string> + <string name="performing_lookup_title">Buscando…</string> + <string name="expand_collapse_all_posts">Expandir/ocultar todos los estados</string> + <string name="action_open_post">Abrir</string> + <string name="restart_required">Reinicio requerido</string> + <string name="restart_emoji">Tendrás que reiniciar la aplicación para aplicar estos cambios</string> + <string name="later">Más tarde</string> + <string name="restart">Reiniciar</string> + <string name="caption_systememoji">Conjunto de emoji predeterminado de su dispositivo</string> + <string name="caption_blobmoji">Los emojis de Android 4.4 a 7.1</string> + <string name="caption_twemoji">Pack estándar de Emojis de Mastodon</string> + <string name="download_failed">Descarga fallida</string> + <string name="profile_badge_bot_text">Bot</string> + <string name="account_moved_description">%1$s se trasladó a:</string> + <string name="reblog_private">Volver a compartir</string> + <string name="unreblog_private">Dejar de compartir</string> + <string name="license_description">Tusky contiene código y recursos de los siguientes proyectos:</string> + <string name="license_apache_2">Licenciado bajo Apache License (texto bajo la lista)</string> + <string name="license_cc_by_4">CC BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Información adicional</string> + <string name="profile_metadata_add">añadir campo</string> + <string name="profile_metadata_label_label">Etiqueta</string> + <string name="profile_metadata_content_label">Contenido</string> + <string name="pref_title_absolute_time">Usar tiempo absoluto</string> + <string name="label_remote_account">La información de abajo puede mostrar el perfil del usuario incompleto. Pulse para abrir el perfil completo en el navegador.</string> + <string name="unpin_action">No fijar</string> + <string name="pin_action">Fijar</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Favorito</item> + <item quantity="many"><b>%1$s</b> Favoritos</item> + <item quantity="other"><b>%1$s</b> Favoritos</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> Impulso</item> + <item quantity="many"><b>%1$s</b> Impulsos</item> + <item quantity="other"><b>%1$s</b> Impulsos</item> + </plurals> + <string name="title_reblogged_by">Impulsado por</string> + <string name="title_favourited_by">Marcado como favorito por</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s y %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s y %3$d más</string> + <string name="action_mentions">Menciones</string> + <string name="action_open_faved_by">Mostrar favoritos</string> + <string name="title_mentions_dialog">Menciones</string> + <string name="download_media">Descargar multimedia</string> + <string name="app_theme_system">Usar tema del sistema</string> + <string name="pref_title_language">Idioma</string> + <string name="action_unreblog">Dejar de impulsar</string> + <string name="action_unfavourite">Eliminar favorito</string> + <string name="pref_title_timeline_filters">Filtros</string> + <string name="notification_poll_name">Encuestas</string> + <string name="filter_dialog_remove_button">Eliminar</string> + <string name="action_create_list">Crear una lista</string> + <string name="action_rename_list">Renombrar la lista</string> + <string name="action_add_to_list">Añadir cuenta a la lista</string> + <string name="action_remove_from_list">Eliminar cuenta de la lista</string> + <string name="description_post_favourited">1Favoritos</string> + <string name="description_visibility_private">Seguidores</string> + <string name="filter_apply">Aplicar</string> + <string name="pref_title_bot_overlay">Mostrar indicador de bots</string> + <string name="poll_vote">Votar</string> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d día restante</item> + <item quantity="many">%1$d días restantes</item> + <item quantity="other">%1$d días restante</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d hora restante</item> + <item quantity="many">%1$d horas restantes</item> + <item quantity="other">%1$d horas restantes</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d minuto restante</item> + <item quantity="many">%1$d minutos restantes</item> + <item quantity="other">%1$d minutos restantes</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d segundo restante</item> + <item quantity="many">%1$d segundos restantes</item> + <item quantity="other">%1$d segundos restantes</item> + </plurals> + <string name="poll_info_format"> <!-- 15 votos • queda 1 hora --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s voto</item> + <item quantity="many">%1$s votos</item> + <item quantity="other">%1$s votos</item> + </plurals> + <string name="poll_info_closed">cerrada</string> + <string name="action_delete_and_redraft">Eliminar y editar</string> + <string name="action_links">Enlaces</string> + <string name="action_hashtags">Etiquetas</string> + <string name="title_hashtags_dialog">Etiquetas</string> + <string name="title_links_dialog">Enlaces</string> + <string name="action_open_media_n">Abrir contenido #%1$d</string> + <string name="downloading_media">Descargando contenido</string> + <string name="dialog_redraft_post_warning">¿Eliminar y devolver a borradores este toot\?</string> + <string name="pref_title_notification_filter_poll">encuestas han terminado</string> + <string name="notification_poll_description">Notificaciones sobre encuestas que han terminado</string> + <string name="pref_title_public_filter_keywords">Cronologías públicas</string> + <string name="pref_title_thread_filter_keywords">Conversaciones</string> + <string name="filter_addition_title">Añadir filtro</string> + <string name="filter_edit_title">Editar filtro</string> + <string name="filter_dialog_update_button">Actualizar</string> + <string name="filter_add_description">Frase para filtrar</string> + <string name="error_create_list">No se pudo crear la lista</string> + <string name="error_rename_list">No se pudo renombrar la lista</string> + <string name="error_delete_list">No se pudo eliminar la lista</string> + <string name="action_delete_list">Eliminar la lista</string> + <string name="hint_search_people_list">Buscar personas que sigues</string> + <string name="description_post_media">Contenido: %1$s</string> + <string name="description_post_cw">Aviso de contenido: %1$s</string> + <string name="description_post_media_no_description_placeholder">Sin descripción</string> + <string name="description_post_reblogged">Compartido</string> + <string name="description_visibility_public">Público</string> + <string name="description_visibility_unlisted">Sin listar</string> + <string name="description_visibility_direct">Directo</string> + <string name="hint_list_name">Nombre de la lista</string> + <string name="edit_hashtag_hint">Etiqueta sin #</string> + <string name="notifications_clear">Limpiar</string> + <string name="notifications_apply_filter">Filtro</string> + <string name="compose_shortcut_long_label">Escribir publicación</string> + <string name="compose_shortcut_short_label">Redactar</string> + <string name="notification_clear_text">¿Estás seguro de que quieres eliminar permanentemente todas tus notificaciones\?</string> + <string name="compose_preview_image_description">Acciones para la imagen %1$s</string> + <string name="poll_info_time_absolute">termina en %1$s</string> + <string name="poll_ended_voted">Una encuesta en la que has votado ha terminado</string> + <string name="poll_ended_created">Una encuesta que has creado ha terminado</string> + <string name="action_open_reblogger">Abrir autor del impulso</string> + <string name="action_open_reblogged_by">Mostrar compartidos</string> + <string name="title_domain_mutes">Dominios ocultos</string> + <string name="action_view_domain_mutes">Dominios ocultos</string> + <string name="action_mute_domain">Silenciar %1$s</string> + <string name="confirmation_domain_unmuted">%1$s visible</string> + <string name="mute_domain_warning">¿Estás seguro de que quieres bloquear todo sobre %1$s\? No verás contenido de ese dominio ni en la cronología ni en tus notificaciones. Tus seguidores de ese dominio serán eliminados.</string> + <string name="mute_domain_warning_dialog_ok">Ocultar dominio completo</string> + <string name="pref_title_animate_gif_avatars">Animar avatares GIF</string> + <string name="filter_dialog_whole_word">Toda palabra</string> + <string name="filter_dialog_whole_word_description">Cuando la palabra o frase sea solo alfanumérica, solo se aplicará si coincide con toda la palabra</string> + <string name="caption_notoemoji">Set de emojis actual de Google</string> + <string name="description_poll">Encuesta con opciones: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="button_continue">Continuar</string> + <string name="button_back">Atrás</string> + <string name="button_done">Hecho</string> + <string name="report_sent_success">\@%1$s reportado satisfactoriamente</string> + <string name="hint_additional_info">Comentarios adicionales</string> + <string name="report_remote_instance">Reenviar a %1$s</string> + <string name="failed_report">Reporte fallido</string> + <string name="failed_fetch_posts">Fallo al obtener estados</string> + <string name="report_description_1">El reporte será enviado a un moderador de tu servidor. Puedes añadir una explicación de por qué estás reportando esta cuenta a continuación:</string> + <string name="report_description_remote_instance">La cuenta es de otro servidor. ¿Enviar una copia anónima del reporte\?</string> + <string name="pref_title_alway_open_spoiler">Mostrar siempre publicaciones marcadas con avisos de contenido</string> + <string name="title_accounts">Cuentas</string> + <string name="failed_search">Error al buscar</string> + <string name="action_add_poll">Añadir encuesta</string> + <string name="create_poll_title">Encuesta</string> + <string name="duration_5_min">5 minutos</string> + <string name="duration_30_min">30 minutos</string> + <string name="duration_1_hour">1 hora</string> + <string name="duration_6_hours">6 horas</string> + <string name="duration_1_day">1 día</string> + <string name="duration_3_days">3 días</string> + <string name="duration_7_days">7 días</string> + <string name="add_poll_choice">Añadir opción</string> + <string name="poll_allow_multiple_choices">Opciones múltiples</string> + <string name="poll_new_choice_hint">Opción %1$d</string> + <string name="edit_poll">Editar</string> + <string name="title_scheduled_posts">Estados programados</string> + <string name="action_edit">Editar</string> + <string name="action_access_scheduled_posts">Estados programados</string> + <string name="action_schedule_post">Programar estado</string> + <string name="action_reset_schedule">Reiniciar</string> + <string name="post_lookup_error_format">Error al buscar el post %1$s</string> + <string name="about_powered_by_tusky">Potenciado por Tusky</string> + <string name="title_bookmarks">Marcadores</string> + <string name="action_bookmark">Marcador</string> + <string name="action_view_bookmarks">Marcadores</string> + <string name="description_post_bookmarked">Marcado como favorito</string> + <string name="select_list_title">Seleccionar lista</string> + <string name="list">Lista</string> + <string name="no_drafts">No tienes ningún borrador.</string> + <string name="no_scheduled_posts">No tienes ningún estado programado.</string> + <string name="warning_scheduling_interval">Mastodon tiene un intervalo de programación mínimo de 5 minutos.</string> + <string name="notification_follow_request_name">Solicitudes</string> + <string name="dialog_block_warning">Bloquear @%1$s\?</string> + <string name="dialog_mute_warning">Silenciar @%1$s\?</string> + <string name="action_mute_conversation">Silenciar conversación</string> + <string name="action_unmute_conversation">Dejar de silenciar conversación</string> + <string name="notification_follow_request_description">Notificaciones de solicitudes</string> + <string name="pref_title_notification_filter_follow_requests">solicitud de seguimiento</string> + <string name="pref_title_confirm_reblogs">Pedir confirmación antes de impulsar</string> + <string name="pref_title_show_cards_in_timelines">Mostrar previsualización de enlaces en la cronología</string> + <string name="pref_title_enable_swipe_for_tabs">Habilitar gesto de deslizar para alternar entre pestañas</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s persona</item> + <item quantity="many">%1$s personas</item> + <item quantity="other">%1$s personas</item> + </plurals> + <string name="hashtags">Etiquetas</string> + <string name="add_hashtag_title">Añadir etiqueta</string> + <string name="notification_follow_request_format">%1$s solicita seguirte</string> + <string name="pref_main_nav_position_option_bottom">Abajo</string> + <string name="pref_main_nav_position_option_top">Arriba</string> + <string name="pref_main_nav_position">Posición de navegación principal</string> + <string name="pref_title_gradient_for_media">Mostrar degradados colorido para los medios ocultos</string> + <string name="action_unmute_domain">Dejar de silenciar a %1$s</string> + <string name="action_unmute_desc">Dejar de silenciar a %1$s</string> + <string name="dialog_mute_hide_notifications">Ocultar notificaciones</string> + <string name="pref_title_hide_top_toolbar">Ocultar el título de la barra de herramientas superior</string> + <string name="account_note_saved">¡Guardado!</string> + <string name="account_note_hint">Tu nota privada acerca de esta cuenta</string> + <string name="no_announcements">No hay anuncios.</string> + <string name="title_announcements">Anuncios</string> + <string name="notification_subscription_format">%1$s recién publicado</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">No puedes cargar más de %1$d archivo multimedia adjunto.</item> + <item quantity="many">No puedes cargar más de %1$d archivos multimedia adjuntos.</item> + <item quantity="other">No puedes cargar más de %1$d archivos multimedia adjuntos.</item> + </plurals> + <string name="wellbeing_hide_stats_profile">Esconder las estadísticas cuantitativas de los perfiles</string> + <string name="wellbeing_hide_stats_posts">Esconder las estadísticas cuantitativas de las publicaciones</string> + <string name="review_notifications">Revisar Notificaciones</string> + <string name="pref_title_wellbeing_mode">Bienestar</string> + <string name="notification_subscription_description">Notificaciones cuando alguien al que estoy suscrito escribe una publicación</string> + <string name="notification_subscription_name">Nuevas publicaciones</string> + <string name="pref_title_notification_filter_subscriptions">alguien al que estoy suscrito hizo una nueva publicación</string> + <string name="wellbeing_mode_notice">Se ocultarán algunas informaciones que podrían afectar a tu bienestar. Esto incluye: +\n +\n- Notificaciones de favoritos, impulsos y seguidores +\n- Conteo de favoritos e impulsos en publicaciones +\n- Estadísticas de seguidores y publicaciones en perfiles +\n +\nLas notificaciones Push no serán afectadas, pero puedes revisar manualmente tus preferencias.</string> + <string name="drafts_post_reply_removed">El toot al que redactaste una respuesta ha sido eliminado</string> + <string name="draft_deleted">Borrador eliminado</string> + <string name="drafts_failed_loading_reply">Error al cargar la información de respuesta</string> + <string name="drafts_post_failed_to_send">¡Este toot no se pudo enviar!</string> + <string name="dialog_delete_list_warning">¿Realmente quieres eliminar la lista %1$s\?</string> + <string name="duration_indefinite">Indefinido</string> + <string name="label_duration">Duración</string> + <string name="post_media_attachments">Adjuntos</string> + <string name="post_media_audio">Audio</string> + <string name="limit_notifications">Limitar cronología de notificaciones</string> + <string name="action_unbookmark">Quitar marcador</string> + <string name="follow_requests_info">Aunque su cuenta no está bloqueada, el personal de %1$s pensó que podría querer revisar las solicitudes de seguimiento de estas cuentas manualmente.</string> + <string name="action_subscribe_account">Suscribir</string> + <string name="dialog_delete_conversation_warning">¿Eliminar esta conversación\?</string> + <string name="pref_title_animate_custom_emojis">Animar emojis personalizados</string> + <string name="action_unsubscribe_account">Darse de baja</string> + <string name="action_delete_conversation">Eliminar conversación</string> + <string name="pref_title_confirm_favourites">Pedir confirmación antes de marcar como favorito</string> + <string name="pref_title_notification_filter_updates">una publicación con la que interactué se editó</string> + <string name="error_multimedia_size_limit">Los archivos de video y audio no pueden pesar más de %1$s MB.</string> + <string name="error_image_edit_failed">Esta imagen no puede ser editada.</string> + <string name="pref_show_self_username_always">Siempre</string> + <string name="pref_show_self_username_never">Nunca</string> + <string name="action_add_reaction">añadir reacción</string> + <string name="pref_title_notification_filter_sign_ups">alguien se registró</string> + <string name="error_following_hashtag_format">Error al seguir #%1$s</string> + <string name="error_unfollowing_hashtag_format">Error dejando de seguir #%1$s</string> + <string name="title_login">Ingreso</string> + <string name="title_migration_relogin">Reingresa para activar notificaciones push</string> + <string name="notification_sign_up_format">%1$s se registró</string> + <string name="notification_update_format">%1$s editó su publicación</string> + <string name="action_dismiss">Descartar</string> + <string name="action_details">Detalles</string> + <string name="error_loading_account_details">Fallo cargando los detalles de la cuenta</string> + <string name="error_could_not_load_login_page">Fallo cargando la página de ingreso.</string> + <string name="delete_scheduled_post_warning">¿Eliminar publicación programada\?</string> + <string name="set_focus_description">Toca o arrastra el círculo para centrar el foco de la imagen, que será visible en las miniaturas.</string> + <string name="compose_save_draft_loses_media">¿Guardar este borrador\? (Los adjuntos se subirán de nuevo cuando vuelvas a él.)</string> + <string name="pref_title_show_self_username">Mostrar nombre de usuario en la barra de herramientas</string> + <string name="account_date_joined">Se unió %1$s</string> + <string name="duration_14_days">14 días</string> + <string name="duration_365_days">365 días</string> + <string name="tips_push_notification_migration">Inicia sesión de nuevo en todas las cuentas para activar las notificaciones push.</string> + <string name="dialog_push_notification_migration">Para poder usar las notificaciones push con UnifiedPush, Tusky necesita permiso para suscribirse a las notificaciones de tu servidor de Mastodon. Es necesario volver a acceder para cambiar los parámetros OAuth concedidos a Tusky. Usar aquí, o en las Preferencias de la cuenta, la opción de volver a acceder conservará los borradores y la caché.</string> + <string name="failed_to_pin">Fallo al fijar</string> + <string name="failed_to_unpin">Fallo al quitarlo</string> + <string name="pref_show_self_username_disambiguate">Cuando hay varias cuentas ingresadas</string> + <string name="notification_sign_up_description">Notificaciones de nuevos usuarios</string> + <string name="notification_update_name">Ediciones de una publicación</string> + <string name="notification_update_description">Notificaciones cuando se editan publicaciones con las que has interactuado</string> + <string name="status_count_one_plus">1+</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="action_set_focus">Establece el foco</string> + <string name="description_post_language">Idioma de publicación</string> + <string name="duration_30_days">30 días</string> + <string name="duration_60_days">60 días</string> + <string name="duration_90_days">90 días</string> + <string name="duration_180_days">180 días</string> + <string name="duration_no_change">(Sin cambios)</string> + <string name="tusky_compose_post_quicksetting_label">Escribir publicación</string> + <string name="saving_draft">Guardando borrador…</string> + <string name="dialog_push_notification_migration_other_accounts">Has vuelto a iniciar sesión en esta cuenta para dar permiso de notificaciones push a Tusky. Sin embargo, aún hay otras cuentas que no tienen este permiso. Cambia a estas cuentas y vuelve a iniciar sesión, una a una, para activar el soporte de notificaciones de UnifiedPush.</string> + <string name="instance_rule_info">Al iniciar sesión aceptas las normas de %1$s.</string> + <string name="instance_rule_title">Normas de %1$s</string> + <string name="notification_sign_up_name">Creación de cuentas</string> + <string name="action_edit_image">Editar imagen</string> + <string name="failed_to_add_to_list">Fallo al añadir la cuenta a la lista</string> + <string name="failed_to_remove_from_list">Fallo al eliminar la cuenta de la lista</string> + <string name="action_add_or_remove_from_list">Añadir o quitar de la lista</string> + <string name="hint_media_description_missing">El contenido debería tener una descripción.</string> + <string name="report_category_violation">Violación de una regla</string> + <string name="report_category_spam">Spam</string> + <string name="pref_default_post_language">Idioma de publicación por defecto</string> + <string name="pref_title_http_proxy_port_message">El puerto debe situarse entre %1$d y %2$d</string> + <string name="notification_report_name">Denuncias</string> + <string name="description_post_edited">Editado</string> + <string name="notification_report_format">Nueva denuncia en %1$s</string> + <string name="notification_header_report_format">%1$s denunció a %2$s</string> + <string name="notification_summary_report_format">%1$s · %2$d publicaciones fijadas</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="error_muting_hashtag_format">Error al silenciar #%1$s</string> + <string name="error_unmuting_hashtag_format">Error al reactivar #%1$s</string> + <string name="no_lists">No tienes ninguna lista.</string> + <string name="notification_report_description">Notificaciones sobre informes de moderación</string> + <string name="error_following_hashtags_unsupported">Esta instancia no soporta el seguimiento de etiquetas.</string> + <string name="title_followed_hashtags">Etiquetas seguidas</string> + <string name="report_category_other">Otros</string> + <string name="post_media_alt">ALT</string> + <string name="action_unfollow_hashtag_format">¿Dejar de seguir a #%1$s\?</string> + <string name="post_edited">%1$s editado</string> + <string name="error_status_source_load">Fallo al cargar el estado desde el servidor.</string> + <string name="confirmation_hashtag_unfollowed">Dejaste de seguir a #%1$s</string> + <string name="pref_title_notification_filter_reports">Hay una nueva denuncia</string> + <string name="action_discard">Descartar cambios</string> + <string name="action_continue_edit">Continuar editando</string> + <string name="compose_unsaved_changes">Tienes cambios sin guardar.</string> + <string name="title_edits">Ediciones</string> + <string name="action_post_failed">La subida falló</string> + <string name="action_post_failed_do_nothing">Descartar</string> + <string name="action_browser_login">Iniciar sesión con el navegador</string> + <string name="a11y_label_loading_thread">Cargando hilo</string> + <string name="mute_notifications_switch">Silenciar notificaciones</string> + <string name="pref_title_reading_order">Orden de lectura</string> + <string name="pref_reading_order_oldest_first">Más antiguas primero</string> + <string name="pref_reading_order_newest_first">Más recientes primero</string> + <string name="status_edit_info">%1$s ha editado</string> + <string name="status_created_info">%1$s ha editado</string> + <string name="pref_summary_http_proxy_disabled">Desactivado</string> + <string name="pref_summary_http_proxy_missing"><sin establecer></string> + <string name="pref_summary_http_proxy_invalid"><inválido></string> + <string name="action_post_failed_detail">No se ha podido subir tu publicación y se ha guardado en Borradores. +\n +\nO no se ha podido contactar con el servidor, o ha rechazado tu publicación.</string> + <string name="action_post_failed_detail_plural">Tus publicaciones no se han podido subir y se han guardado en Borradores. +\n +\nO no se ha podido contactar con el servidor o, o ha rechazado tus publicaciones.</string> + <string name="action_post_failed_show_drafts">Mostrar borradores</string> + <string name="send_account_username_to">Compartir nombre de usuario de la cuenta a…</string> + <string name="account_username_copied">Nombre de usuario copiado</string> + <string name="description_browser_login">Puede soportar métodos de autenticación adicionales, pero hace falta un navegador soportado.</string> + <string name="description_login">Funcionar en la mayoría de los casos. No se comparten datos con otras apps.</string> + <string name="action_share_account_username">Compartir el nombre de usuario de la cuenta</string> + <string name="send_account_link_to">Compartir URL de la cuenta con…</string> + <string name="action_share_account_link">Compartir enlace a la cuenta</string> + <string name="title_public_trending_hashtags">Hashtags en tendencia</string> + <string name="accessibility_talking_about_tag">%1$d personas están hablando de #%2$s</string> + <string name="total_usage">Uso total</string> + <string name="total_accounts">Cuentas totales</string> + <string name="dialog_follow_hashtag_title">Seguir etiqueta</string> + <string name="dialog_follow_hashtag_hint">#etiqueta</string> + <string name="post_media_image">Imagen</string> + <string name="pref_title_account_filter_keywords">Perfiles</string> + <string name="action_refresh">Volver a cargar</string> + <string name="notification_unknown_name">Desconocido</string> + <string name="status_filtered_show_anyway">Mostrar de todas formas</string> + <string name="status_filter_placeholder_label_format">Filtrado: %1$s</string> + <string name="pref_title_show_stat_inline">Mostrar estadísticas de la entrada en la línea de tiempo</string> + <string name="ui_error_unknown">razón desconocida</string> + <string name="socket_timeout_exception">Contactar con tu servidor ha tardado demasiado tiempo</string> + <string name="help_empty_home">Esta es tu <b> cronología de inicio</b>. Muestra las publicaciones recientes de las cuentas que sigues. +\n +\nPara encontrar cuentas, puedes mirar en alguna de las otras cronologías. Por ejemplo, la cronología local de tu instancia [iconics gmd_group]. O puedes buscarlas por nombre [iconics gmd_search]; por ejemplo, busca Tusky para encontrar nuestra cuenta de Mastodon.</string> + <string name="ui_error_bookmark">Fallo al añadir a marcadores: %1$s</string> + <string name="select_list_manage">Gestionar listas</string> + <string name="ui_error_favourite">Fallo al favoritear publicación: %1$s</string> + <string name="ui_error_clear_notifications">Fallo al limpiar notificaciones: %1$s</string> + <string name="ui_error_accept_follow_request">Fallo al aceptar solicitud de seguimiento: %1$s</string> + <string name="ui_error_reject_follow_request">Fallo al rechazar la solicitud de seguimiento: %1$s</string> + <string name="ui_success_accepted_follow_request">Solicitud de seguimiento aceptada</string> + <string name="ui_success_rejected_follow_request">Solicitud de seguimiento bloqueada</string> + <string name="hint_filter_title">Mi filtro</string> + <string name="filter_edit_keyword_title">Editar palabra</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="ui_error_reblog">Fallo al impulsar publicación: %1$s</string> + <string name="ui_error_vote">Fallo al votar en la encuesta: %1$s</string> + <string name="label_filter_title">Título</string> + <string name="filter_action_warn">Aviso</string> + <string name="filter_description_hide">Ocultar completamente</string> + <string name="filter_description_warn">Ocultar con un aviso</string> + <string name="filter_action_hide">Ocultar</string> + <string name="label_filter_action">Acción del filtro</string> + <string name="label_filter_context">Lugares donde se aplica el filtro</string> + <string name="label_filter_keywords">Palabras y frases clave para filtrar</string> + <string name="action_add">Añadir</string> + <string name="filter_keyword_display_format">%1$s (palabra completa)</string> + <string name="filter_keyword_addition_title">Añadir palabra</string> + <string name="pref_ui_text_size">Tamaño de fuente</string> + <string name="notification_notification_worker">Obteniendo notificaciones…</string> + <string name="load_newest_notifications">Cargar nuevas notificaciones</string> + <string name="compose_delete_draft">¿Eliminar borrador\?</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml new file mode 100644 index 0000000..614e9a4 --- /dev/null +++ b/app/src/main/res/values-eu/strings.xml @@ -0,0 +1,538 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Errorea gertatu da.</string> + <string name="error_empty">Eremu hau ezin da hutsik egon.</string> + <string name="error_invalid_domain">Domeinu baliogabea sartu da</string> + <string name="error_failed_app_registration">Akatsa instantzia horrekin autentikatzerakoan. Akatsak badarrai, menutik, nabigatzailean saioa hasteko aukerarekin saiatu.</string> + <string name="error_no_web_browser_found">Ez da web nabigatzailerik aurkitu.</string> + <string name="error_authorization_unknown">Identifikatu gabeko baimen-akatsa gertatu da. Akatsak badarrai, menutik, nabigatzailean saioa hasteko aukerarekin saiatu.</string> + <string name="error_authorization_denied">Baimena ukatu da. Ziur bazaude zuk sartutako egiaztagiriak zuzenak direla, menutik, nabigatzailean saioa hasteko aukerarekin saiatu.</string> + <string name="error_retrieving_oauth_token">Akatsa saio-hasieraren identifikatzailea eskuratzerakoan. Akatsak badarrai, menutik, nabigatzailean saioa hasteko aukerarekin saiatu.</string> + <string name="error_compose_character_limit">Tut luzeegia!</string> + <string name="error_media_upload_type">Ez da fitxategi mota hau onartzen.</string> + <string name="error_media_upload_opening">Ezin izan da fitxategi hau ireki.</string> + <string name="error_media_upload_permission">Memoriara sartzeko baimena behar da.</string> + <string name="error_media_download_permission">Memoriara deskargatzeko baimena behar da.</string> + <string name="error_media_upload_image_or_video">Ezin dira bideoak eta irudiak tut berean gehitu.</string> + <string name="error_media_upload_sending">Akatsa igotzerakoan.</string> + <string name="error_sender_account_gone">Akatsa tuta bidaltzerakoan.</string> + <string name="title_home">Hasiera</string> + <string name="title_notifications">Jakinarazpenak</string> + <string name="title_public_local">Bertakoa</string> + <string name="title_public_federated">Federatua</string> + <string name="title_view_thread">Haria</string> + <string name="title_posts">Tutak</string> + <string name="title_posts_with_replies">Erantzunekin</string> + <string name="title_follows">Jarraitzen</string> + <string name="title_followers">Jarraitzaileak</string> + <string name="title_favourites">Gogokoak</string> + <string name="title_mutes">Isilduak</string> + <string name="title_blocks">Blokeatuak</string> + <string name="title_follow_requests">Jarraipen-eskaerak</string> + <string name="title_edit_profile">Profila editatu</string> + <string name="title_drafts">Zirriborroak</string> + <string name="title_licenses">Lizentziak</string> + <string name="post_boosted_format">%1$s-(e)k bultzatu du</string> + <string name="post_sensitive_media_title">Eduki hunkigarria</string> + <string name="post_media_hidden_title">Ezkutuko media</string> + <string name="post_sensitive_media_directions">Sakatu ikusteko</string> + <string name="post_content_warning_show_more">Gehiago erakutsi</string> + <string name="post_content_warning_show_less">Gutxiago erakutsi</string> + <string name="post_content_show_more">Zabaldu</string> + <string name="post_content_show_less">Itxi</string> + <string name="footer_empty">Edukirik ez. Arrastatu behera birkargatzeko!</string> + <string name="notification_reblog_format">%1$s-(e)k zure tuta bultzatu du</string> + <string name="notification_favourite_format">%1$s-(e)k zure tuta gogoko du</string> + <string name="notification_follow_format">%1$s-(e)k jarraitu zaitu</string> + <string name="report_username_format">\@%1$s salatu</string> + <string name="report_comment_hint">Informazio gehigarria?</string> + <string name="action_quick_reply">Erantzun azkarra</string> + <string name="action_reply">Erantzun</string> + <string name="action_reblog">Bultzatu</string> + <string name="action_favourite">Gogokoa</string> + <string name="action_more">Gehiago</string> + <string name="action_compose">Idatzi</string> + <string name="action_login">Tuskykin saioa hasi</string> + <string name="action_logout">Saioa itxi</string> + <string name="action_logout_confirm">Ziur zaude %1$s saioa itxi nahi duzula?</string> + <string name="action_follow">Jarraitu</string> + <string name="action_unfollow">Jarraitzeari utzi</string> + <string name="action_block">Blokeatu</string> + <string name="action_unblock">Desblokeatu</string> + <string name="action_hide_reblogs">Bultzadak ezkutatu</string> + <string name="action_show_reblogs">Bultzadak erakutsi</string> + <string name="action_report">Salatu</string> + <string name="action_delete">Ezabatu</string> + <string name="action_send">Tut</string> + <string name="action_send_public">TUT!</string> + <string name="action_retry">Berriz saiatu</string> + <string name="action_close">Itxi</string> + <string name="action_view_profile">Profila</string> + <string name="action_view_preferences">Ezarpenak</string> + <string name="action_view_account_preferences">Kontuaren ezarpenak</string> + <string name="action_view_favourites">Gogokoak</string> + <string name="action_view_mutes">Isilduak</string> + <string name="action_view_blocks">Blokeatuak</string> + <string name="action_view_follow_requests">Eskakizunak</string> + <string name="action_view_media">Multimedia</string> + <string name="action_open_in_web">Nabigatzailean ireki</string> + <string name="action_add_media">Multimedia erantsi</string> + <string name="action_photo_take">Argazkia atera</string> + <string name="action_share">Partekatu</string> + <string name="action_mute">Mututu</string> + <string name="action_unmute">Desmututu</string> + <string name="action_mention">Aipatu</string> + <string name="action_hide_media">Multimedia ezkutatu</string> + <string name="action_open_drawer">Tiradera ireki</string> + <string name="action_save">Gorde</string> + <string name="action_edit_profile">Profila editatu</string> + <string name="action_edit_own_profile">Editatu</string> + <string name="action_undo">Desegin</string> + <string name="action_accept">Onartu</string> + <string name="action_reject">Ukatu</string> + <string name="action_search">Bilatu</string> + <string name="action_access_drafts">Zirriborroak</string> + <string name="action_toggle_visibility">Tutaren ikusgarritasuna</string> + <string name="action_content_warning">Edukiaren abisua</string> + <string name="action_emoji_keyboard">Emoji teklatua</string> + <string name="download_image">%1$s jaisten</string> + <string name="action_copy_link">Lotura kopiatu</string> + <string name="send_post_link_to">Tutaren URL partekatu…</string> + <string name="send_post_content_to">Tuta partekatu…</string> + <string name="send_media_to">Partekatu media hona…</string> + <string name="confirmation_reported">Bidalia!</string> + <string name="confirmation_unblocked">Erabiltzailea desblokeatuta</string> + <string name="confirmation_unmuted">Erabiltzailea isilgabetuta</string> + <string name="hint_domain">Zein instantzia\?</string> + <string name="hint_compose">Zer duzu buruan?</string> + <string name="hint_content_warning">Edukiaren abisua</string> + <string name="hint_display_name">Agertuko den izena</string> + <string name="hint_note">Biografia</string> + <string name="hint_search">Bilatu…</string> + <string name="search_no_results">Emaitzarik ez</string> + <string name="label_quick_reply">Erantzun…</string> + <string name="label_avatar">Irudia</string> + <string name="label_header">Goiburua</string> + <string name="link_whats_an_instance">Zer da instantzia?</string> + <string name="login_connection">Konektatzen…</string> + <string name="dialog_whats_an_instance">Edozein instantziaren helbidea edo domeinua hemen sar daiteke, hala nola mastodon.eus, mastodon.jalgi.eus, mastodon.social eta <a href="https://instances.social">gehiago!</a>, +\n +\nOraindik konturik ez baduzu, sartu nahi duzun instantziaren izena sar dezakezu eta bertan sortu kontua. +\n +\nInstantzia zure kontua ostatatzen den leku bakarra da, baina beste instantzia batzuetako jendearekin erraz komunikatu eta jarrai dezakezu gune berean egongo bazina bezala. +\n +\nInformazio gehiago <a href="https://joinmastodon.org">joinmastodon.org</a> webgunean aurki daiteke. </string> + <string name="dialog_title_finishing_media_upload">Mediaren igoera bukatzen</string> + <string name="dialog_message_uploading_media">Igotzen…</string> + <string name="dialog_download_image">Jaitsi</string> + <string name="dialog_message_cancel_follow_request">Jarraipenaren eskakizunari uko egin\?</string> + <string name="dialog_unfollow_warning">Kontu hau jarraitzeari utzi\?</string> + <string name="dialog_delete_post_warning">Tut hau ezabatu\?</string> + <string name="visibility_public">Publikoa: Istorio publikoetan erakutsi</string> + <string name="visibility_unlisted">Ezkutukoa: Ez erakutsi istorio publikoetan</string> + <string name="visibility_private">Pribatua: Jarraitzaileentzat soilik ikusgai</string> + <string name="visibility_direct">Zuzena: Aipatutako kontuentzat bakarrik ikusgai</string> + <string name="pref_title_edit_notification_settings">Jakinarazpenak</string> + <string name="pref_title_notifications_enabled">Jakinarazpenak</string> + <string name="pref_title_notification_alerts">Alertak</string> + <string name="pref_title_notification_alert_sound">Soinuarekin jakinarazi</string> + <string name="pref_title_notification_alert_vibrate">Bibrazioarekin jakinarazi</string> + <string name="pref_title_notification_alert_light">Led-arekin jakinarazi</string> + <string name="pref_title_notification_filters">Honen arabera jakinarazi</string> + <string name="pref_title_notification_filter_mentions">Aipatzen naute</string> + <string name="pref_title_notification_filter_follows">Jarraitzen didate</string> + <string name="pref_title_notification_filter_reblogs">Bultzatzen naute</string> + <string name="pref_title_notification_filter_favourites">Nire argitarapenak gustokoak izan dira</string> + <string name="pref_title_appearance_settings">Interfazea</string> + <string name="pref_title_app_theme">Gaia</string> + <string name="pref_title_timelines">Denbora-lerroak</string> + <string name="app_them_dark">Iluna</string> + <string name="app_theme_light">Argia</string> + <string name="app_theme_black">Beltza</string> + <string name="app_theme_auto">Automatikoa</string> + <string name="pref_title_browser_settings">Nabigatzailea</string> + <string name="pref_title_custom_tabs">Chromeko fitxak erabili</string> + <string name="pref_title_post_filter">Denbora-lerroaren iragaztea</string> + <string name="pref_title_post_tabs">Fitxak</string> + <string name="pref_title_show_boosts">Bultzadak erakutsi</string> + <string name="pref_title_show_replies">Erakutsi erantzunak</string> + <string name="pref_title_show_media_preview">Jaitsi mediaren aurreikuspenak</string> + <string name="pref_title_proxy_settings">Proxya</string> + <string name="pref_title_http_proxy_settings">HTTP Proxy-a</string> + <string name="pref_title_http_proxy_enable">HTTP Proxy-a gaitu</string> + <string name="pref_title_http_proxy_server">HTTP Proxy-aren zerbitzaria</string> + <string name="pref_title_http_proxy_port">HTTP Proxy-aren portua</string> + <string name="pref_default_post_privacy">Aurrezarritako ikusgarritasuna</string> + <string name="pref_default_media_sensitivity">Beti markatu multimedia eduki mingarri gisa</string> + <string name="pref_publishing">Bidalketak</string> + <string name="pref_failed_to_sync">Aukerak sinkronizatzean akatsa</string> + <string name="post_privacy_public">Publiko</string> + <string name="post_privacy_unlisted">Zerrendagabetuta</string> + <string name="post_privacy_followers_only">Jarraitzaileak soilik</string> + <string name="pref_post_text_size">Tutaren testuaren tamaina</string> + <string name="post_text_size_smallest">Txikiena</string> + <string name="post_text_size_small">Txikia</string> + <string name="post_text_size_medium">Ertaina</string> + <string name="post_text_size_large">Handia</string> + <string name="post_text_size_largest">Handiena</string> + <string name="notification_mention_name">Aipamen berriak</string> + <string name="notification_mention_descriptions">Aipamen berrien jakinarazpenak</string> + <string name="notification_follow_name">Jarritzaile berriak</string> + <string name="notification_follow_description">Jarraitzaile berrien jakinarazpenak</string> + <string name="notification_boost_name">Bultzadak</string> + <string name="notification_boost_description">Bultzatutako tuten jakinarazpenak</string> + <string name="notification_favourite_name">Gogokoak</string> + <string name="notification_favourite_description">Zure tutak gogoko bezala ezartzerakoan jakinarazpenak</string> + <string name="notification_mention_format">%1$s-(e)k aipatu zaitu</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s eta beste %4$d</string> + <string name="notification_summary_medium">%1$s, %2$s eta %3$s</string> + <string name="notification_summary_small">%1$s eta %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">interakzio berri %1$d</item> + <item quantity="other">%1$d interakzio berri</item> + </plurals> + <string name="description_account_locked">Kontu babestua</string> + <string name="about_title_activity">Honi buruz</string> + <string name="about_tusky_license">Tusky software libre eta kode askekoa da. + \"GNU General Public License Version 3\" lizentziapean zabaldua. + Lizentzia hontaz gehiago irakurtzeko: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site"> Proiektuaren gunea:\n + https://tusky.app + </string> + <string name="about_bug_feature_request_site">Akatsen berri-emateak eta hobekuntza-eskariak: +\n https://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Tuskyren profila</string> + <string name="post_share_content">Tutaren edukia partekatu</string> + <string name="post_share_link">Tutaren esteka partekatu</string> + <string name="post_media_images">Irudiak</string> + <string name="post_media_video">Bideoa</string> + <string name="state_follow_requested">Eskaera bidalita</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">%1$du-an</string> + <string name="abbreviated_in_days">%1$de-an</string> + <string name="abbreviated_in_hours">%1$dh-an</string> + <string name="abbreviated_in_minutes">%1$dm-an</string> + <string name="abbreviated_in_seconds">%1$ds-an</string> + <string name="abbreviated_years_ago">%1$du</string> + <string name="abbreviated_days_ago">%1$de</string> + <string name="follows_you">Jarraitzen zaitu</string> + <string name="pref_title_alway_show_sensitive_media">Eduki mingarria erakutsi</string> + <string name="title_media">Multimedia</string> + <string name="replying_to">\@%1$s-(r)i erantzuten</string> + <string name="load_more_placeholder_text">Gehiago erakutsi</string> + <string name="add_account_name">Gehitu kontua</string> + <string name="add_account_description">Mastodon kontua gehitu</string> + <string name="action_lists">Zerrendak</string> + <string name="title_lists">Zerrendak</string> + <string name="compose_active_account_description">%1$s kontuarekin tut egiten</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">Ikusmen urritasuna dutenentzat deskribapena\n(%1$d karaktereko muga)</item> + </plurals> + <string name="action_set_caption">Deskribapena erantsi</string> + <string name="action_remove">Ezabatu</string> + <string name="lock_account_label">Kontua babestu</string> + <string name="lock_account_label_description">Jarraitzaileak eskuz onartu beharko dituzu</string> + <string name="compose_save_draft">Zirriborroa gorde?</string> + <string name="send_post_notification_title">Tuta bidaltzen…</string> + <string name="send_post_notification_error_title">Errorea tuta bidaltzean</string> + <string name="send_post_notification_channel_name">Tuta bidaltzen</string> + <string name="send_post_notification_cancel_title">Bidalketa bertan behera utzita</string> + <string name="send_post_notification_saved_content">Tutaren kopia bat zure zirriborroetan gorde da</string> + <string name="action_compose_shortcut">Idatzi</string> + <string name="error_no_custom_emojis">%1$s instantziak ez ditu emoji pertsonalizatuak eskaintzen</string> + <string name="emoji_style">Emojien estiloa</string> + <string name="system_default">Sistema</string> + <string name="download_fonts">Lehenago jaitsi beharko dituzu</string> + <string name="performing_lookup_title">Bilatzen…</string> + <string name="expand_collapse_all_posts">Tut guztiak zabaldu/itxi</string> + <string name="action_open_post">Tuta ireki</string> + <string name="restart_required">Berrabiaraztea beharrezkoa da</string> + <string name="restart_emoji">Aplikazioa berrabiarazi beharko duzu aldaketa ezartzeko</string> + <string name="later">Beranduago</string> + <string name="restart">Berrabiarazi</string> + <string name="caption_systememoji">Zure gailuko defektuzko emojiak</string> + <string name="caption_blobmoji">Android 4.4etik 7.1erako emoji-ak</string> + <string name="caption_twemoji">Mastodoneko emoji pakete estandarra</string> + <string name="download_failed">Deskargatzerakoan akatsa</string> + <string name="profile_badge_bot_text">Bot-a</string> + <string name="account_moved_description">%1$s hona mugitu da:</string> + <string name="reblog_private">Berriz bultzatu</string> + <string name="unreblog_private">Bultzatzea utzi</string> + <string name="license_description">Tusky-k ondorengo proiektuetako kode eta baliabideak ditu:</string> + <string name="license_apache_2">Apache Lizentziapean (testua zerrendaren azpian)</string> + <string name="profile_metadata_label">Informazio gehigarria</string> + <string name="profile_metadata_add">Eremua gehitu</string> + <string name="profile_metadata_label_label">Etiketa</string> + <string name="profile_metadata_content_label">Edukia</string> + <string name="pref_title_absolute_time">Denbora absolutua erabili</string> + <string name="label_remote_account">Ondorengo edukiak erabiltzailearen informazioa erdizka erakutsi dezake. Profil osoa ikusteko nabigatzailean sakatu.</string> + <string name="unpin_action">Desainguratu</string> + <string name="pin_action">Ainguratu</string> + <string name="error_network">Sareko errore bat sortu da! Zure konexioa ziurta ezazu berriro, mesedez!</string> + <string name="title_direct_messages">Mezu zuzenak</string> + <string name="title_tab_preferences">Fitxak</string> + <string name="title_posts_pinned">Finkatua</string> + <string name="title_domain_mutes">Ezkutuko domeinuak</string> + <string name="title_scheduled_posts">Programatutako tutak</string> + <string name="post_username_format">\@%1$s</string> + <string name="message_empty">Kilkerrak besterik ez hemen.</string> + <string name="action_unreblog">Bultzada kendu</string> + <string name="action_unfavourite">Gogokoa kendu</string> + <string name="action_edit">Editatu</string> + <string name="action_delete_and_redraft">Ezabatu eta zirriborroa berriro egin</string> + <string name="action_view_domain_mutes">Ezkutuko domeinuak</string> + <string name="action_add_poll">Galdeketa gehitu</string> + <string name="action_mute_domain">Mututu %1$s</string> + <string name="action_access_scheduled_posts">Programatutako tutak</string> + <string name="action_schedule_post">Tuta programatu</string> + <string name="action_reset_schedule">Berrezarri</string> + <string name="action_add_tab">Kategoria gehitu</string> + <string name="action_links">Estekak</string> + <string name="action_mentions">Aipamenak</string> + <string name="action_open_reblogged_by">Bultzadak erakutsi</string> + <string name="action_open_faved_by">Gogokoak erakutsi</string> + <string name="title_mentions_dialog">Aipamenak</string> + <string name="title_links_dialog">Estekak</string> + <string name="action_open_media_n">Ireki media #%1$d</string> + <string name="action_open_as">%1$s bezala ireki</string> + <string name="action_share_as">… bezala partekatu</string> + <string name="download_media">Media jaisten</string> + <string name="downloading_media">Media jaisten</string> + <string name="confirmation_domain_unmuted">%1$s ez dago ezkutatua</string> + <string name="dialog_redraft_post_warning">Tut hau ezabatu eta zirriborro berria egin\?</string> + <string name="mute_domain_warning">Ziur al zaude %1$s-(r)en eduki guztia ezabatu nahi duzula\? Domeinu horretatik datorren edukia ez duzu denbora-lerro publikoetan edo jakinarazpenetan ikusiko. Domeinu horretan dituzun jarraitzaileak ezabatuko dira.</string> + <string name="mute_domain_warning_dialog_ok">Domeinu osoa ezkutatu</string> + <string name="pref_title_notification_filter_poll">Galdeketak bukatu dira</string> + <string name="pref_title_timeline_filters">Iragazkiak</string> + <string name="app_theme_system">Erabili sistemaren diseinua</string> + <string name="pref_title_language">Hizkuntza</string> + <string name="pref_title_bot_overlay">Botentzako erakuslea erakutsi</string> + <string name="pref_title_animate_gif_avatars">GIF abatarrak animatu</string> + <string name="notification_poll_name">Galdeketak</string> + <string name="notification_poll_description">Bukatutako galdeketen jakinarazpenak</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="action_hashtags">Traolak</string> + <string name="title_hashtags_dialog">Traolak</string> + <string name="about_powered_by_tusky">Tusky-k sustatuta</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="pref_title_alway_open_spoiler">Beti zabaldu edukien abisuekin markatutako tootak</string> + <string name="pref_title_thread_filter_keywords">Elkarrizketak</string> + <string name="filter_addition_title">Gehitu iragazkia</string> + <string name="filter_edit_title">Editatu iragazkia</string> + <string name="filter_dialog_remove_button">Ezabatu</string> + <string name="filter_dialog_update_button">Eguneratu</string> + <string name="filter_dialog_whole_word">Hitz osoa</string> + <string name="filter_dialog_whole_word_description">Gako-hitza edo esaldia alfanumerikoa denean bakarrik, hitz osoarekin bat datorrenean bakarrik aplikatuko da</string> + <string name="filter_add_description">Iragazteko esaldia</string> + <string name="error_create_list">Ezin izan da zerrenda sortu</string> + <string name="error_rename_list">Ezin izan da zerrendaren izena aldatu</string> + <string name="error_delete_list">Ezin izan da zerrenda ezabatu</string> + <string name="action_create_list">Zerrenda sortu</string> + <string name="action_rename_list">Zerrenda berrizendatu</string> + <string name="action_delete_list">Ezabatu zerrenda</string> + <string name="hint_search_people_list">Bilatu jarraitzen dituzun pertsonak</string> + <string name="action_add_to_list">Gehitu kontua zerrendan</string> + <string name="action_remove_from_list">Kendu kontua zerrendatik</string> + <string name="caption_notoemoji">Google-ren egungo emoji multzoa</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <plurals name="favs"> + <item quantity="one">Gogoko <b>%1$s</b></item> + <item quantity="other"><b>%1$s</b> Gogoko</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one">Bultzada <b>%1$s</b></item> + <item quantity="other"><b>%1$s</b> Bultzada</item> + </plurals> + <string name="title_reblogged_by">Bultzatuta</string> + <string name="title_favourited_by">Gogokoa</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s eta %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s eta %3$d gehiago</string> + <string name="description_post_media">Media: %1$s</string> + <string name="description_post_cw">Edukiarekiko abisua: %1$s</string> + <string name="description_post_media_no_description_placeholder">Deskribapenik ez</string> + <string name="description_post_reblogged">Partekatua</string> + <string name="description_post_favourited">Gogokoa</string> + <string name="description_visibility_public">Publikoa</string> + <string name="description_visibility_unlisted">Zerrendatu gabea</string> + <string name="description_visibility_private">Jarraitzaileak</string> + <string name="description_visibility_direct">Zuzena</string> + <string name="description_poll">Inkestatu aukerekin: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="hint_list_name">Zerrendaren izena</string> + <string name="edit_hashtag_hint">Traola # gabe</string> + <string name="notifications_clear">Garbitu</string> + <string name="notifications_apply_filter">Iragazi</string> + <string name="filter_apply">Aplikatu</string> + <string name="compose_shortcut_long_label">Idatzi tuta</string> + <string name="compose_shortcut_short_label">Idatzi</string> + <string name="notification_clear_text">Ziur zaude jakinarazpen guztiak betirako garbitu nahi dituzula\?</string> + <string name="compose_preview_image_description">%1$s irudiarentzako ekintzak</string> + <string name="poll_info_format"> <!-- 15 boto • Ordu 1 geratzen da --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">Boto %1$s</item> + <item quantity="other">%1$s boto</item> + </plurals> + <string name="poll_info_time_absolute">%1$s amaitzen da</string> + <string name="poll_info_closed">Itxita</string> + <string name="poll_vote">Botatu</string> + <string name="poll_ended_voted">Botoa eman duzun galdeketa amaitu da</string> + <string name="poll_ended_created">Sortu duzun galdeketa amaitu da</string> + <plurals name="poll_timespan_days"> + <item quantity="one">Egun %1$d geratzen da</item> + <item quantity="other">%1$d egun geratzen da</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">Ordu %1$d geratzen da</item> + <item quantity="other">%1$d ordu geratzen da</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">Segundu %1$d geratzen da</item> + <item quantity="other">%1$d segundu geratzen da</item> + </plurals> + <string name="button_continue">Jarraitu</string> + <string name="button_back">Itzuli</string> + <string name="button_done">Eginda</string> + <string name="report_sent_success">\@%1$s jakinarazi duzu arrakastaz</string> + <string name="hint_additional_info">Iruzkin gehigarriak</string> + <string name="report_remote_instance">%1$s(r)i birbidali</string> + <string name="failed_report">Txostena huts egin du</string> + <string name="failed_fetch_posts">Akatsa tutak eskuratzean</string> + <string name="report_description_1">Txostena zure zerbitzariaren moderatzaileari bidaliko zaio. Jarraian, kontu honen zergatia salatzen duzun azalpena eman dezakezu:</string> + <string name="report_description_remote_instance">Kontua beste zerbitzari batekoa da. Bidali txostenaren kopia anonimatua hara ere\?</string> + <string name="title_accounts">Kontuak</string> + <string name="failed_search">Bilaketa huts egin du</string> + <string name="create_poll_title">Inkesta</string> + <string name="duration_5_min">5 minutu</string> + <string name="duration_30_min">30 minutu</string> + <string name="duration_1_hour">Ordu 1</string> + <string name="duration_6_hours">6 ordu</string> + <string name="duration_1_day">Egun 1</string> + <string name="duration_3_days">3 egun</string> + <string name="duration_7_days">7 egun</string> + <string name="add_poll_choice">Gehitu aukera</string> + <string name="poll_allow_multiple_choices">Aukera anitzak</string> + <string name="poll_new_choice_hint">%1$d. aukera</string> + <string name="edit_poll">Editatu</string> + <string name="post_lookup_error_format">Errorea agertu da %1$s mezua bilatzean</string> + <string name="title_bookmarks">Laster-markak</string> + <string name="action_bookmark">Laster-marka</string> + <string name="action_view_bookmarks">Laster-markak</string> + <string name="action_open_reblogger">Ireki bultzadaren egilea</string> + <string name="pref_title_public_filter_keywords">Denbora lerro publikoak</string> + <string name="description_post_bookmarked">Laster-marketara gehitua</string> + <string name="select_list_title">Aukeratu zerrenda</string> + <string name="list">Zerrenda</string> + <string name="no_drafts">Ez duzu zirriborrorik.</string> + <string name="no_scheduled_posts">Ez duzu tut programaturik.</string> + <string name="warning_scheduling_interval">Mastodonek gutxienez 5 minutuko programazio-tartea du.</string> + <string name="notification_follow_request_name">Eskakizunak</string> + <string name="notification_follow_request_description">Jarraitzeko eskaereri buruzko jakinarazpenak</string> + <string name="dialog_mute_warning">\@%1$s isildu\?</string> + <string name="dialog_block_warning">\@%1$s blokeatu\?</string> + <string name="action_mute_conversation">Elkarrizketa mututu</string> + <string name="notification_follow_request_format">%1$s-(e)k zu jarraitzeko eskatu zaitu</string> + <string name="hashtags">Traolak</string> + <string name="dialog_mute_hide_notifications">Jakinarazpenak ezkutatu</string> + <string name="action_unmute_desc">Desmututu %1$s</string> + <string name="pref_title_hide_top_toolbar">Goiko tresna-barraren izenburua ezkutatu</string> + <string name="pref_title_confirm_reblogs">Tuta bultzatu aurretik berresteko galdetu</string> + <string name="pref_title_show_cards_in_timelines">Esteken aurrebista denbora-lerroetan erakutsi</string> + <string name="pref_title_enable_swipe_for_tabs">Irristatzeko keinua gaitu fitxetan zehar mugitzeko</string> + <plurals name="poll_timespan_minutes"> + <item quantity="one">Minutu %1$d faltan</item> + <item quantity="other">%1$d minutu faltan</item> + </plurals> + <string name="add_hashtag_title">Traola gehitu</string> + <string name="pref_main_nav_position_option_bottom">Beheran</string> + <string name="pref_main_nav_position_option_top">Goian</string> + <string name="pref_main_nav_position">Nabigazio nagusiaren posizioa</string> + <string name="pref_title_gradient_for_media">Gradiente koloretsuak erakutsi ezkutuko mediaren lekuan</string> + <string name="pref_title_notification_filter_follow_requests">jarraitzeko eskaera jasotzean</string> + <string name="action_unmute_conversation">Elkarrizketa desmututu</string> + <string name="action_unmute_domain">Desmututu %1$s</string> + <string name="review_notifications">Jakinarazpenak berrikusi</string> + <string name="pref_title_confirm_favourites">Erakutsi baieztapen elkarrizketa-koadroa gogokoenetara gehitu aurretik</string> + <string name="follow_requests_info">Zure kontua blokeatuta ez badago ere, %1$s-ko langileek kontu hauetako eskaerak eskuz berrikusi nahi dituzula pentsatu dute.</string> + <string name="pref_title_notification_filter_subscriptions">harpidedun naizen norbaitek tut berria argitaratu du</string> + <string name="post_media_attachments">Eranskinak</string> + <string name="dialog_delete_list_warning">Ziur %1$s zerrenda ezabatu nahi duzula\?</string> + <string name="post_media_audio">Audioa</string> + <string name="action_subscribe_account">Harpidetu</string> + <string name="dialog_delete_conversation_warning">Elkarrizketa ezabatu nahi duzu\?</string> + <string name="pref_title_animate_custom_emojis">Animatu emoji pertsonalizatuak</string> + <string name="drafts_failed_loading_reply">Erantzunaren informazioa ezin izan da kargatu</string> + <string name="wellbeing_hide_stats_profile">Profiletan estatistika kuantitatiboak ezkutatu</string> + <string name="draft_deleted">Zirriborroa ezabatu da</string> + <string name="drafts_post_reply_removed">Erantzun nahi zenuen tuta ezabatua izan da</string> + <string name="pref_title_wellbeing_mode">Ongizatea</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Ezin duzu multimedia eranskin %1$d baino gehiago kargatu.</item> + <item quantity="other">Ezin dituzu %1$d multimedia eranskin baino gehiago kargatu.</item> + </plurals> + <string name="limit_notifications">Denbora-lerroaren jakinarazpenak mugatu</string> + <string name="label_duration">Iraupena</string> + <string name="duration_indefinite">Zehaztugabea</string> + <string name="action_unsubscribe_account">Harpidetza kendu</string> + <string name="action_delete_conversation">Elkarrizketa ezabatu</string> + <string name="no_announcements">Ez daude iragarkirik.</string> + <string name="notification_subscription_format">%1$s argitaratu berri du</string> + <string name="account_note_saved">Gordeta!</string> + <string name="notification_subscription_name">Tut berriak</string> + <plurals name="poll_info_people"> + <item quantity="one">Pertsona %1$s</item> + <item quantity="other">%1$s pertsona</item> + </plurals> + <string name="title_announcements">Iragarpenak</string> + <string name="notification_subscription_description">Jakinarazpenak harpidetuta zauden norbaitek tut berria argitaratu duenean</string> + <string name="account_note_hint">Kontu honi buruzko zure ohar pribatua</string> + <string name="drafts_post_failed_to_send">Akatsa tut hau bidaltzean!</string> + <string name="wellbeing_mode_notice">Zure ongizate mentalean eragina izan dezaketen zenbait informazio ezkutatuta egongo dira. Honek honako hauek ditu: +\n +\n - Gogokoak, bultzadak eta jarraitzaileen jakinarazpenak +\n - Tutetan gogokoen eta bultzaden kopurua +\n - Profiletan jarraitzaileen eta argitalpenen estatistikak +\n +\nPush-jakinarazpenek ez dute eraginik izango, baina jakinarazpenen hobespenak eskuz berrikus ditzakezu.</string> + <string name="wellbeing_hide_stats_posts">Mezuetan estatistika kuantitatiboak ezkutatu</string> + <string name="action_unbookmark">Laster-marka kendu</string> + <string name="error_image_edit_failed">Ezin izan da irudia editatu.</string> + <string name="error_multimedia_size_limit">Bideo eta audio fitxategiek ezin dute %1$s MBeko tamaina baino handiagoa izan.</string> + <string name="error_muting_hashtag_format">Akatsa #%1$s mututzerakoan</string> + <string name="error_unmuting_hashtag_format">Errorea #%1$s desmututzerakoan</string> + <string name="error_following_hashtags_unsupported">Instantzia honek traolak jarraitzeko funtzioarekin bateragarritasuna ez dauka.</string> + <string name="error_following_hashtag_format">Akatsa #%1$s jarraitzerakoan</string> + <string name="error_unfollowing_hashtag_format">Akatsa #%1$s jarraitzerakoan</string> + <string name="error_could_not_load_login_page">Ezin izan da saio-hasierako orria kargatu.</string> + <string name="error_loading_account_details">Akatsa kontuaren xehetasunak kargatzerakoan</string> + <string name="title_login">Saioa hasi</string> + <string name="title_edits">Aldaketak</string> + <string name="notification_report_format">Salaketa berria %1$s-(r)i</string> + <string name="notification_header_report_format">%1$s-(e)k %2$s salatu du</string> + <string name="notification_summary_report_format">%1$s · %2$d tutak finkatuak</string> + <string name="title_followed_hashtags">Jarraitutako traolak</string> + <string name="notification_update_format">%1$s-(e)k bere tuta editatu du</string> + <string name="title_migration_relogin">Berriro saioa hasi push motako jakinarazpenak gaitzeko</string> + <string name="action_post_failed">Igoerak huts egin du</string> + <string name="action_post_failed_detail">Akatsa zure tuta bidaltzean, beraz, zirriborroetara bidali da. +\n +\nZerbitzariarekin ezin izan da komunikatu edo tuta baztertu egin da.</string> + <string name="action_post_failed_detail_plural">Akatsa zure tutak bidaltzean, beraz, zirriborroetara bidali dira. +\n +\nZerbitzariarekin ezin izan da komunikatu edo zure tutak baztertuak izan dira.</string> + <string name="action_post_failed_show_drafts">Zirriborroak erakutsi</string> + <string name="action_post_failed_do_nothing">Baztertu</string> + <string name="post_media_alt">ALT</string> + <string name="action_browser_login">Nabigatzailearekin saioa hasi</string> + <string name="post_edited">%1$s editatua</string> + <string name="notification_sign_up_format">%1$s-(e)k izena eman du</string> + <string name="error_status_source_load">Akatsa zerbitzaritik egoeraren iturria kargatzean.</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-fa/strings.xml b/app/src/main/res/values-fa/strings.xml new file mode 100644 index 0000000..b5ae68b --- /dev/null +++ b/app/src/main/res/values-fa/strings.xml @@ -0,0 +1,723 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">خطایی رخ داد.</string> + <string name="error_empty">این نمیتواند خالی باشد.</string> + <string name="error_invalid_domain">دامنهٔ نامعتبر وارد شده</string> + <string name="error_failed_app_registration">احراز هویت با این نمونه شکست خورد. اگر مشکل ادامه داشت، ورود در مرورگر را از فهرست بیازمایید.</string> + <string name="error_no_web_browser_found">مرورگری برای استفاده پیدا نشد.</string> + <string name="error_authorization_unknown">خطای احراز هویت ناشناختهای رخ داد. اگر مشکل ادامه داشت، ورود در مرورگر را از فهرست بیازمایید.</string> + <string name="error_authorization_denied">احراز هویت رد شد. اگر مطمئنید که اطّلاعات را درست وارد کردهاید، ورود در مرورگر را از فهرست بیازمایید.</string> + <string name="error_retrieving_oauth_token">دریافت ژتون ورود شکست خورد. اگر مشکل ادامه داشت، ورود در مرورگر را از فهرست بیازمایید.</string> + <string name="error_compose_character_limit">فرسته خیلی طولانی است!</string> + <string name="error_media_upload_type">این گونهٔ پرونده نمیتواند بارگذاری شود.</string> + <string name="error_media_upload_opening">این پرونده نتوانست گشوده شود.</string> + <string name="error_media_upload_permission">نیاز به اجازهٔ خواندن رسانه است.</string> + <string name="error_media_download_permission">نیاز به اجازهٔ ذخیرهٔ رسانه است.</string> + <string name="error_media_upload_image_or_video">تصاویر و فیلمها نمیتوانند به یک فرسته پیوست شوند.</string> + <string name="error_media_upload_sending">بارگذاری شکست خورد.</string> + <string name="error_sender_account_gone">خطای فرستادن فرسته.</string> + <string name="title_home">خانه</string> + <string name="title_notifications">آگاهیها</string> + <string name="title_public_local">محلّی</string> + <string name="title_public_federated">همگانی</string> + <string name="title_view_thread">رشته</string> + <string name="title_posts">فرستهها</string> + <string name="title_posts_with_replies">با پاسخ</string> + <string name="title_follows">دنبال شونده</string> + <string name="title_followers">پیگیر</string> + <string name="title_favourites">برگزیدهها</string> + <string name="title_mutes">کاربران خموش</string> + <string name="title_blocks">کاربران مسدود</string> + <string name="title_follow_requests">درخواستهای پیگیری</string> + <string name="title_edit_profile">ویرایش نمایهتان</string> + <string name="title_drafts">پیشنویسها</string> + <string name="title_licenses">پروانهها</string> + <string name="post_boosted_format">%1$s تقویت کرد</string> + <string name="post_sensitive_media_title">محتوای حسّاس</string> + <string name="post_media_hidden_title">رسانهٔ نهفته</string> + <string name="post_sensitive_media_directions">کلیک برای نمایش</string> + <string name="post_content_warning_show_more">نمایش بیشتر</string> + <string name="post_content_warning_show_less">نمایش کمتر</string> + <string name="post_content_show_more">گسترش</string> + <string name="post_content_show_less">جمع کردن</string> + <string name="footer_empty">اینجا هیچچیز نیست. برای تازهسازی، به پایین بکشید!</string> + <string name="notification_reblog_format">%1$s فرستهتان را تقویت کرد</string> + <string name="notification_favourite_format">%1$s فرستهتان را برگزید</string> + <string name="notification_follow_format">%1$s پیگیرتان شد</string> + <string name="report_username_format">گزارش @%1$s</string> + <string name="report_comment_hint">نظرهای اضافی؟</string> + <string name="action_quick_reply">پاسخ سریع</string> + <string name="action_reply">پاسخ</string> + <string name="action_reblog">تقویت</string> + <string name="action_favourite">برگزیدن</string> + <string name="action_more">بیشتر</string> + <string name="action_compose">ایجاد</string> + <string name="action_login">ورود با تاسکی</string> + <string name="action_logout">خروج</string> + <string name="action_follow">پیگیری</string> + <string name="action_unfollow">ناپیگیری</string> + <string name="action_block">انسداد</string> + <string name="action_unblock">رفع انسداد</string> + <string name="action_hide_reblogs">نهفتن تقویتها</string> + <string name="action_show_reblogs">نمایش تقویتها</string> + <string name="action_report">گزارش</string> + <string name="action_delete">حذف</string> + <string name="action_send">بوق</string> + <string name="action_send_public">بوق!</string> + <string name="action_retry">تلاش دوباره</string> + <string name="action_close">بستن</string> + <string name="action_view_profile">نمایه</string> + <string name="action_view_preferences">ترجیحات</string> + <string name="action_view_account_preferences">ترجیحات حساب</string> + <string name="action_view_favourites">برگزیدهها</string> + <string name="action_view_mutes">کاربران خموش</string> + <string name="action_view_blocks">کاربران مسدود</string> + <string name="action_view_follow_requests">درخواستهای پیگیری</string> + <string name="action_view_media">رسانه</string> + <string name="action_open_in_web">گشودن در مرورگر</string> + <string name="action_add_media">افزودن رسانه</string> + <string name="action_photo_take">گرفتن عکس</string> + <string name="action_share">همرسانی</string> + <string name="action_mute">خموشی</string> + <string name="action_unmute">ناخموشی</string> + <string name="action_mention">اشاره</string> + <string name="action_hide_media">نهفتن رسانه</string> + <string name="action_open_drawer">گشودن کشو</string> + <string name="action_save">ذخیره</string> + <string name="action_edit_profile">ویرایش نمایه</string> + <string name="action_edit_own_profile">ویرایش</string> + <string name="action_undo">بازگشت</string> + <string name="action_accept">پذیرش</string> + <string name="action_reject">رد</string> + <string name="action_search">جستوجو</string> + <string name="action_access_drafts">پیشنویسها</string> + <string name="action_toggle_visibility">نمایانی فرسته</string> + <string name="action_content_warning">هشدار محتوا</string> + <string name="action_emoji_keyboard">صفحهکلید اموجی</string> + <string name="download_image">درحال بارگیری %1$s</string> + <string name="action_copy_link">رونوشت از پیوند</string> + <string name="send_post_link_to">همرسانی نشانی فرسته با…</string> + <string name="send_post_content_to">همرسانی فرسته با…</string> + <string name="send_media_to">همرسانی رسانه با…</string> + <string name="confirmation_reported">فرستاده شد!</string> + <string name="confirmation_unblocked">کاربرنامسدود شد</string> + <string name="confirmation_unmuted">کاربر ناخموش شد</string> + <string name="hint_domain">کدام نمونه؟</string> + <string name="hint_compose">چه خبر؟</string> + <string name="hint_content_warning">هشدار محتوا</string> + <string name="hint_display_name">نام نمایشی</string> + <string name="hint_note">شرح حال</string> + <string name="hint_search">جستوجو…</string> + <string name="search_no_results">بدون هیچ نتیجهای</string> + <string name="label_quick_reply">پاسخ…</string> + <string name="label_avatar">آواتار</string> + <string name="label_header">سرایند</string> + <string name="link_whats_an_instance">نمونه چیست؟</string> + <string name="login_connection">در حال وصل شدن…</string> + <string name="dialog_whats_an_instance">نشانی یا دامنهٔ هر نمونهای میتواند وارد شود، چون mastodon.social, icosahedron.website, social.tchncs.de و <a href="https://instances.social">بیشتر!</a>. +\n \u0020 +\n اگر هنوز حسابی ندارید، میتوانید نام نمونهای که میخواهید به آن بپیوندید را وارد کرده و در آنجا حسابی بسازید. +\n \u0020 +\n نمونه جاییست که حسابتان رویش میزبانی میشود. به راحتی میتوانید با افراد روی نمونههای دیگر ارتباط داشته و دنبالشان کنید؛ انگار که روی یک پایگاه باشید. +\n \u0020 +\nاطّلاعات بیشتر میتواند در <a href="https://joinmastodon.org">joinmastodon.org</a> پیدا شود. \u0020</string> + <string name="dialog_title_finishing_media_upload">پایان بارگذاری رسانه</string> + <string name="dialog_message_uploading_media">در حال بارگذاری…</string> + <string name="dialog_download_image">بارگیری</string> + <string name="dialog_message_cancel_follow_request">درخواست دنبال کردن را لغو میکنید؟</string> + <string name="dialog_unfollow_warning">ناپیگیری این حساب؟</string> + <string name="dialog_delete_post_warning">حذف این فرسته؟</string> + <string name="visibility_public">عمومی: فرستادن به خط زمانیهای عمومی</string> + <string name="visibility_unlisted">فهرستنشده: نشان ندادن در خط زمانیهای عمومی</string> + <string name="visibility_private">تنها دنبالکنندگان:پست فقط به دنبالکنندگان</string> + <string name="visibility_direct">مستقیم: فرستادن فقط برای کاربران اشارهشده</string> + <string name="pref_title_edit_notification_settings">آگاهیها</string> + <string name="pref_title_notifications_enabled">آگاهیها</string> + <string name="pref_title_notification_alerts">هشدارها</string> + <string name="pref_title_notification_alert_sound">آگاهی با صدا</string> + <string name="pref_title_notification_alert_vibrate">آگاهی با لرزش</string> + <string name="pref_title_notification_alert_light">آگاهی با چراغ</string> + <string name="pref_title_notification_filters">به من اطلاع بده زمانی که</string> + <string name="pref_title_notification_filter_mentions">اشاره شدن</string> + <string name="pref_title_notification_filter_follows">دنبالشده</string> + <string name="pref_title_notification_filter_reblogs">فرستههایم تقویت شدند</string> + <string name="pref_title_notification_filter_favourites">فرستههایم برگزیده شدند</string> + <string name="pref_title_appearance_settings">ظاهر</string> + <string name="pref_title_app_theme">زمینهٔ کاره</string> + <string name="pref_title_timelines">خط زمانیها</string> + <string name="app_them_dark">تاریک</string> + <string name="app_theme_light">روشن</string> + <string name="app_theme_black">سیاه</string> + <string name="app_theme_auto">خودکار در غروب</string> + <string name="pref_title_browser_settings">مرورگر</string> + <string name="pref_title_custom_tabs">استفاده از زبانههای سفارشی کروم</string> + <string name="pref_title_post_filter">پالایش خط زمانی</string> + <string name="pref_title_post_tabs">خط زمانی خانه</string> + <string name="pref_title_show_boosts">نمایش تقویتها</string> + <string name="pref_title_show_replies">نمایش پاسخها</string> + <string name="pref_title_show_media_preview">بارگیری پیشنمایش رسانه</string> + <string name="pref_title_proxy_settings">پیشکار</string> + <string name="pref_title_http_proxy_settings">پیشکار HTTP</string> + <string name="pref_title_http_proxy_enable">به کار انداختن پیشکار HTTP</string> + <string name="pref_title_http_proxy_server">کارساز پیشکار HTTP</string> + <string name="pref_title_http_proxy_port">درگاه پیشکار HTTP</string> + <string name="pref_default_post_privacy">محرمانگی پیشگزیدهٔ فرسته (همگام با کارساز)</string> + <string name="pref_default_media_sensitivity">علامتگذاری همیشگی رسانه به عنوان حساس (همگام با کارساز)</string> + <string name="pref_publishing">منتشر کردن</string> + <string name="pref_failed_to_sync">شکست در همگامسازی ترجیحات</string> + <string name="post_privacy_public">عمومی</string> + <string name="post_privacy_unlisted">فهرستنشده</string> + <string name="post_privacy_followers_only">فقط پیگیران</string> + <string name="pref_post_text_size">اندازهٔ متن فرسته</string> + <string name="post_text_size_smallest">کوچکترین</string> + <string name="post_text_size_small">کوچک</string> + <string name="post_text_size_medium">میانه</string> + <string name="post_text_size_large">بزرگ</string> + <string name="post_text_size_largest">بزرگترین</string> + <string name="notification_mention_name">اشارههای جدید</string> + <string name="notification_mention_descriptions">آگاهیها دربارهٔ اشارههای جدید</string> + <string name="notification_follow_name">پیگیران جدید</string> + <string name="notification_follow_description">آگاهیها دربارهٔ پیگیران جدید</string> + <string name="notification_boost_name">تقویتها</string> + <string name="notification_boost_description">آگاهیها هنگام تقویت فرستههایتان</string> + <string name="notification_favourite_name">برگزیدنها</string> + <string name="notification_favourite_description">آگاهیها هنگام برگزیده شدن فرستههایتان</string> + <string name="notification_mention_format">%1$s به شما اشاره کرد</string> + <string name="notification_summary_large">%1$s، %2$s، %3$s و %4$d دیگر</string> + <string name="notification_summary_medium">%1$s، %2$s و %3$s</string> + <string name="notification_summary_small">%1$s و %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d برهمکنش جدید</item> + <item quantity="other">%1$d برهمکنش جدید</item> + </plurals> + <string name="description_account_locked">حساب قفلشده</string> + <string name="about_title_activity">درباره</string> + <string name="about_tusky_license">تاسکی نرمافزاری آزاد است که تحت نگارش ۳ از پروانهٔ جامع همگانی گنو منتشر شده است. پروانه را میتوانید از اینجا ببینید: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">پایگاه وب پروژه: https://tusky.app</string> + <string name="about_bug_feature_request_site">گزارش مشکلات و درخواست ویژگیها: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">نمایهٔ تاسکی</string> + <string name="post_share_content">همرسانی محتوای فرسته</string> + <string name="post_share_link">همرسانی پیوند فرسته</string> + <string name="post_media_images">تصویرها</string> + <string name="post_media_video">ویدیو</string> + <string name="state_follow_requested">تقاضای پیگیری شد</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">در %1$d سال</string> + <string name="abbreviated_in_days">در %1$d روز</string> + <string name="abbreviated_in_hours">در %1$d ساعت</string> + <string name="abbreviated_in_minutes">در %1$d دقیقه</string> + <string name="abbreviated_in_seconds">در %1$d ثانیه</string> + <string name="follows_you">پیگیرتان است</string> + <string name="pref_title_alway_show_sensitive_media">نمایش همیشگی محتوای حساس</string> + <string name="title_media">رسانه</string> + <string name="replying_to">پاسخ دادن به @%1$s</string> + <string name="load_more_placeholder_text">بار کردن بیشتر</string> + <string name="add_account_name">افزودن حساب</string> + <string name="add_account_description">افزودن حساب ماستودون جدید</string> + <string name="action_lists">سیاههها</string> + <string name="title_lists">سیاههها</string> + <string name="compose_active_account_description">فرستادن از طرف %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">توصیف محتوا برای کمبینایان (کران %1$d نویسه)</item> + <item quantity="other">توصیف محتوا برای کمبینایان (کران %1$d نویسه)</item> + </plurals> + <string name="action_set_caption">تنظیم عنوان</string> + <string name="action_remove">برداشتن</string> + <string name="lock_account_label">قفل حساب</string> + <string name="lock_account_label_description">لازم است پیگیران را دستی تأیید کنید</string> + <string name="compose_save_draft">ذخیرهٔ پیشنویس؟</string> + <string name="send_post_notification_title">فرستادن فرسته…</string> + <string name="send_post_notification_error_title">خطا در فرستادن فرسته</string> + <string name="send_post_notification_channel_name">فرستادن فرستهها</string> + <string name="send_post_notification_cancel_title">فرستادن لغو شد</string> + <string name="send_post_notification_saved_content">رونوشتی از فرسته در پیشنویسهایتان ذخیره شد</string> + <string name="action_compose_shortcut">ایجاد</string> + <string name="error_no_custom_emojis">نمونهتان %1$s هیچ اموجی سفارشیای ندارد</string> + <string name="emoji_style">سبک اموجی</string> + <string name="system_default">پیشگزیدهٔ سامانه</string> + <string name="download_fonts">نخست باید این مجموعههای اموجی را بارگیری کنید</string> + <string name="performing_lookup_title">در حال جستوجو…</string> + <string name="expand_collapse_all_posts">گسترش/جمع کردن تمام فرستهها</string> + <string name="action_open_post">گشودن فرسته</string> + <string name="restart_required">نیاز به آغاز دوبارهٔ کاره</string> + <string name="restart_emoji">برای اعمال این تغییرات، نیاز به شروع دوبارهٔ تاسکی دارید</string> + <string name="later">بعداً</string> + <string name="restart">آغاز دوباره</string> + <string name="caption_systememoji">مجموعه اموجی پیشگزیدهٔ افزارهتان</string> + <string name="caption_blobmoji">اموجیهای اندروید ۴.۴ تا ۷.۱</string> + <string name="caption_twemoji">مجموعه اموجی استاندارد ماستودون</string> + <string name="download_failed">شکست در بارگیری</string> + <string name="profile_badge_bot_text">بات</string> + <string name="account_moved_description">%1$s منتقل شد به:</string> + <string name="reblog_private">تقویت برای مخاطب اصلی</string> + <string name="unreblog_private">ناتقویت</string> + <string name="license_description">تاسکی کد و داراییهایی از پروژههای نرمافزار آزاد زیر دارد:</string> + <string name="license_apache_2">ذیل پروانهٔ آپاچی (رونوشت در ادامه)</string> + <string name="profile_metadata_label">فرادادهٔ نمایه</string> + <string name="profile_metadata_add">افزودن داده</string> + <string name="profile_metadata_label_label">برچسب</string> + <string name="profile_metadata_content_label">محتوا</string> + <string name="pref_title_absolute_time">استفاده از زمان مطلق</string> + <string name="label_remote_account">ممکن است اطلاعات زیر نمایهٔ کاربر را ناقص نشان دهد. برای گشودن نمایهٔ کامل در مرورگر، لمس کنید.</string> + <string name="unpin_action">برداشتن سنجاق</string> + <string name="pin_action">سنجاق کردن</string> + <string name="error_network">خطای شبکهای رخ داد. لطفاً اتصالتان را بررسی و دوباره تلاش کنید.</string> + <string name="title_direct_messages">پیامهای مستقیم</string> + <string name="title_tab_preferences">زبانهها</string> + <string name="title_posts_pinned">سنجاق شده</string> + <string name="title_domain_mutes">دامنههای نهفته</string> + <string name="post_username_format">\@%1$s</string> + <string name="message_empty">اینجا هیچچیزی نیست.</string> + <string name="action_unreblog">برداشتن تقویت</string> + <string name="action_unfavourite">برداشتن برگزیدگی</string> + <string name="action_delete_and_redraft">حذف و بازنویسی</string> + <string name="action_view_domain_mutes">دامنههای نهفته</string> + <string name="action_add_poll">افزودن نظرسنجی</string> + <string name="action_mute_domain">خموشی %1$s</string> + <string name="action_add_tab">افزودن زبانه</string> + <string name="action_links">پیوندها</string> + <string name="action_mentions">اشارهها</string> + <string name="action_hashtags">برچسبها</string> + <string name="action_open_reblogger">گشودن تقویتکننده</string> + <string name="action_open_reblogged_by">نمایش تقویتها</string> + <string name="action_open_faved_by">نمایش برگزیدهها</string> + <string name="title_hashtags_dialog">برچسبها</string> + <string name="title_mentions_dialog">اشارهها</string> + <string name="title_links_dialog">پیوندها</string> + <string name="action_open_media_n">گشودن رسانه #%1$d</string> + <string name="action_open_as">گشودن به عنوان %1$s</string> + <string name="action_share_as">همرسانی به عنوان …</string> + <string name="download_media">بارگیری رسانه</string> + <string name="downloading_media">در حال بارگیری رسانه</string> + <string name="confirmation_domain_unmuted">%1$s نانهفته</string> + <string name="dialog_redraft_post_warning">حذف و بازنویسی این فرسته؟</string> + <string name="mute_domain_warning_dialog_ok">نهفتن تمام دامنه</string> + <string name="pref_title_notification_filter_poll">پایان نظرسنجیها</string> + <string name="pref_title_timeline_filters">پالایهها</string> + <string name="app_theme_system">استفاده از طراحی سامانه</string> + <string name="pref_title_language">زبان</string> + <string name="pref_title_bot_overlay">نمایش نشانگر برای باتها</string> + <string name="pref_title_animate_gif_avatars">پویانمایی آواتارهای جیف</string> + <string name="notification_poll_name">نظرسنجیها</string> + <string name="notification_poll_description">آگاهیها دربارهٔ نظرسنجیهای پایانیافته</string> + <string name="about_tusky_version">تاسکی %1$s</string> + <string name="abbreviated_years_ago">%1$d سال</string> + <string name="abbreviated_days_ago">%1$d روز</string> + <string name="abbreviated_hours_ago">%1$d ساعت</string> + <string name="abbreviated_minutes_ago">%1$d دقیقه</string> + <string name="abbreviated_seconds_ago">%1$d ثانیه</string> + <string name="pref_title_alway_open_spoiler">گسترش همیشگی فرستههای علامتخورده با هشدار محتوا</string> + <string name="pref_title_public_filter_keywords">خط زمانیهای عمومی</string> + <string name="pref_title_thread_filter_keywords">گفتوگوها</string> + <string name="filter_addition_title">افزودن پالایه</string> + <string name="filter_edit_title">ویرایش پالایه</string> + <string name="filter_dialog_remove_button">برداشتن</string> + <string name="filter_dialog_update_button">بهروز رسانی</string> + <string name="filter_dialog_whole_word">تمام واژه</string> + <string name="error_create_list">نتوانست سیاهه را ایجاد کند</string> + <string name="error_rename_list">نتوانست سیاهه را بهروز کند</string> + <string name="error_delete_list">نتوانست سیاهه را حذف کند</string> + <string name="action_create_list">ایجاد سیاهه</string> + <string name="action_rename_list">بهروز رسانیسیاهه</string> + <string name="action_delete_list">حذف سیاهه</string> + <string name="hint_search_people_list">جستوجوی افرادی که پی میگیرید</string> + <string name="action_add_to_list">افزودن حساب به سیاهه</string> + <string name="action_remove_from_list">برداشتن حساب از سیاهه</string> + <string name="caption_notoemoji">مجموعه اموجی کنونی گوگل</string> + <string name="license_cc_by_4">نگارش ۴٫۰ CC-BY</string> + <string name="license_cc_by_sa_4">نگارش ۴٫۰ CC-BY-SA</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> برگزیدن</item> + <item quantity="other"><b>%1$s</b> برگزیدن</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> تقویت</item> + <item quantity="other"><b>%1$s</b> تقویت</item> + </plurals> + <string name="title_reblogged_by">تقویتشده به دست</string> + <string name="title_favourited_by">برگزیده به دست</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s و %2$s</string> + <string name="conversation_more_recipients">%1$s، %2$s و %3$d بیشتر</string> + <string name="description_post_media">رسانه: %1$s</string> + <string name="description_post_cw">هشدار محتوا: %1$s</string> + <string name="description_post_media_no_description_placeholder">بدون شرح</string> + <string name="description_post_reblogged">تقویت شده</string> + <string name="description_post_favourited">برگزیده</string> + <string name="description_visibility_public">عمومی</string> + <string name="description_visibility_unlisted">فهرست نشده</string> + <string name="description_visibility_private">پیگیران</string> + <string name="description_visibility_direct">مستقیم</string> + <string name="description_poll">نظرسنجی با گزینهها: %1$s، %2$s، %3$s، %4$s؛ %5$s</string> + <string name="hint_list_name">نام سیاهه</string> + <string name="edit_hashtag_hint">برچسب بدون #</string> + <string name="notifications_clear">حذف</string> + <string name="notifications_apply_filter">پالایش</string> + <string name="filter_apply">اعمال</string> + <string name="compose_shortcut_long_label">ایجاد فرسته</string> + <string name="compose_shortcut_short_label">ایجاد</string> + <string name="notification_clear_text">مطمئنید میخواهید تمام آگاهیهایتان را برای همیشه پاک کنید؟</string> + <string name="poll_info_time_absolute">پایان در %1$s</string> + <string name="poll_info_closed">بسته</string> + <string name="poll_vote">رأی</string> + <string name="poll_ended_voted">یک نظرسنجی که در آن رأی دادید، تمام شد</string> + <string name="poll_ended_created">یک نظرسنجی که ساختید، تمام شد</string> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d روز مانده</item> + <item quantity="other">%1$d روز مانده</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d ساعت مانده</item> + <item quantity="other">%1$d ساعت مانده</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d دقیقه مانده</item> + <item quantity="other">%1$d دقیقه مانده</item> + </plurals> + <string name="button_continue">ادامه</string> + <string name="button_back">بازگشت</string> + <string name="button_done">تمام</string> + <string name="report_sent_success">@%1$s با موفّقیت گزارش شد</string> + <string name="hint_additional_info">نظرهای اضافی</string> + <string name="report_remote_instance">هدایت به %1$s</string> + <string name="failed_report">شکست در گزارش</string> + <string name="failed_fetch_posts">شکست در واکشی فرستهها</string> + <string name="title_accounts">حسابها</string> + <string name="failed_search">شکست در جستوجو</string> + <string name="create_poll_title">نظرسنجی</string> + <string name="duration_5_min">۵ دقیقه</string> + <string name="duration_30_min">۳۰ دقیقه</string> + <string name="duration_1_hour">۱ ساعت</string> + <string name="duration_6_hours">۶ ساعت</string> + <string name="duration_1_day">۱ روز</string> + <string name="duration_3_days">۳ روز</string> + <string name="duration_7_days">۷ روز</string> + <string name="add_poll_choice">افزودن گزینه</string> + <string name="poll_allow_multiple_choices">گزینههای چندگانه</string> + <string name="poll_new_choice_hint">گزینهٔ %1$d</string> + <string name="edit_poll">ویرایش</string> + <string name="title_scheduled_posts">فرستههای زمانبسته</string> + <string name="action_edit">ویرایش</string> + <string name="action_access_scheduled_posts">فرستههای زمانبسته</string> + <string name="action_schedule_post">فرستهٔ زمانبسته</string> + <string name="action_reset_schedule">بازنشانی</string> + <string name="mute_domain_warning">مطمئنید میخواهید تمام %1$s را مسدود کنید؟ محتوای آن دامنه را در هیچیک از خط زمانیها یا در آگاهیهایتان نخواهید دید. پیگیرانتان از آن دامنه، برداشته خواهند شد.</string> + <string name="filter_dialog_whole_word_description">هنگامی که کلیدواژه یا عبارت، فقط حروفعددی باشد، فقط اگر با تمام واژه مطابق باشد، اعمال خواهد شد</string> + <string name="filter_add_description">عبارت پالایش</string> + <string name="poll_info_format"> \u0020.<!-- ۱۵ رأی • ۱ ساعت مانده --> \u0020%1$s • %2$s</string> + <string name="report_description_1">گزارش به ناظرهای کارسازتان ارسال خواهد شد. در زیر میتوانید توضیحی در باب چرایی گزارش این حساب بنویسید:</string> + <string name="report_description_remote_instance">این حساب از کارسازی دیگر است. رونوشتی ناشناس از گزارش، به آنجا نیز ارسال شود؟</string> + <string name="post_lookup_error_format">خطا در یافتن فرستهٔ %1$s</string> + <string name="about_powered_by_tusky">قدرتگرفته از تاسکی</string> + <string name="title_bookmarks">نشانکها</string> + <string name="action_bookmark">نشانک</string> + <string name="action_view_bookmarks">نشانکها</string> + <string name="description_post_bookmarked">نشانشده</string> + <string name="select_list_title">گزینش سیاهه</string> + <string name="list">سیاهه</string> + <string name="no_drafts">هیچ پیشنویسی ندارید.</string> + <string name="no_scheduled_posts">هیچ فرستهٔ زمانبستهای ندارید.</string> + <string name="warning_scheduling_interval">ماستودون، بازهٔ زمانبندیای با کمینهٔ ۵ دقیقه دارد.</string> + <string name="pref_title_confirm_reblogs">نمایش تأیید پیش از تقویت</string> + <string name="pref_title_show_cards_in_timelines">پیشنمایش پیوندها در خطزمانیها</string> + <string name="pref_title_enable_swipe_for_tabs">به کار انداختن اشارهٔ کشیدنی برای تعویض بین زبانهها</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s نفر</item> + <item quantity="other">%1$s نفر</item> + </plurals> + <string name="hashtags">برچسبها</string> + <string name="add_hashtag_title">افزودن برچسب</string> + <string name="notification_follow_request_description">آگاهیها دربارهٔ درخواستهای پیگیری</string> + <string name="notification_follow_request_name">درخواستهای پیگیری</string> + <string name="pref_title_notification_filter_follow_requests">درخواست پیگیری</string> + <string name="dialog_mute_warning">خموشی @%1$s؟</string> + <string name="dialog_block_warning">انسداد @%1$s؟</string> + <string name="action_unmute_conversation">ناخموشی گفتوگو</string> + <string name="action_mute_conversation">خموشی گفتوگو</string> + <string name="notification_follow_request_format">%1$s میخواهد پیگیرتان شود</string> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d ثانیه مانده</item> + <item quantity="other">%1$d ثانیه مانده</item> + </plurals> + <string name="pref_main_nav_position_option_bottom">پایین</string> + <string name="pref_main_nav_position_option_top">بالا</string> + <string name="pref_main_nav_position">موقعیت ناوبری اصلی</string> + <string name="pref_title_gradient_for_media">نمایش گرادیانهای رنگی برای رسانههای نهفته</string> + <string name="dialog_mute_hide_notifications">نهفتن آگاهیها</string> + <string name="action_unmute_domain">ناخموشی %1$s</string> + <string name="action_unmute_desc">ناخموشی %1$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s رأی</item> + <item quantity="other">%1$s رأی</item> + </plurals> + <string name="pref_title_hide_top_toolbar">نهفتن عنوان نوارابزار بالایی</string> + <string name="compose_preview_image_description">کنشها برای تصویر %1$s</string> + <string name="account_note_saved">ذخیره شد!</string> + <string name="account_note_hint">یادداشت خصوصیتان دربارهٔ این حساب</string> + <string name="no_announcements">هیچ اعلامیهای وجود ندارد.</string> + <string name="title_announcements">اعلامیهها</string> + <string name="action_unsubscribe_account">نااشتراک</string> + <string name="action_subscribe_account">اشتراک</string> + <string name="draft_deleted">پیشنویس حذف شد</string> + <string name="drafts_post_failed_to_send">فرستادن این فرسته شکست خورد!</string> + <string name="wellbeing_hide_stats_profile">نهفتن آمار کمی روی نمایهها</string> + <string name="wellbeing_hide_stats_posts">نهفتن آمار کمی روی فرستهها</string> + <string name="limit_notifications">محدود کردن آگاهیهای خطزمانی</string> + <string name="review_notifications">بازبینی آگاهیها</string> + <string name="pref_title_wellbeing_mode">سلامتی</string> + <string name="label_duration">طول</string> + <string name="post_media_attachments">پیوستها</string> + <string name="post_media_audio">صدا</string> + <string name="notification_subscription_description">آگاهیها هنگام انتشار فرستهای جدید از کسی که پیمیگیرید</string> + <string name="notification_subscription_name">فرستههای جدید</string> + <string name="pref_title_animate_custom_emojis">اموجیهای شخصی متحرّک</string> + <string name="pref_title_notification_filter_subscriptions">کسی که پیمیگیرم، فرستهای جدید منتشر کرد</string> + <string name="notification_subscription_format">%1$s چیزی فرستاد</string> + <string name="drafts_post_reply_removed">فرستهای که پاسخی به آن را پیشنویس کردید، برداشته شده</string> + <string name="drafts_failed_loading_reply">شکست در بار کردن اطّلاعات پاسخ</string> + <string name="wellbeing_mode_notice">برخی اطّلاعات که ممکن است روی سلامتی ذهنیتان تأثیر بگذارد، پنهان خواهند شد. همچون: +\n +\n - آگاهیهای برگزیدن، تقویت و پیگیری +\n - شمار برگزیدن و تقویت فرستهها +\n - آمار پیگیر و فرسته روی نمایهها +\n +\n فرستادن آگاهیها تأثیر نمیپذیرد، ولی میتوانید ترجیحات آگاهیتان را به صورت دستی بازبینی کنید.</string> + <string name="dialog_delete_list_warning">واقعاً میخواهید سیاههٔ %1$s را حذف کنید؟</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">نمیتوانید بیش از %1$d رسانه بارگذارید.</item> + <item quantity="other">نمیتوانید بیش از %1$d رسانه بارگذارید.</item> + </plurals> + <string name="duration_indefinite">نامعیّن</string> + <string name="follow_requests_info">با این که حسابتان قفل نیست، کارکنان %1$s فکر کردند ممکن است بخواهید درخواستهای پیگیری از این حسابها را دستی بازبینی کنید.</string> + <string name="dialog_delete_conversation_warning">حذف این گفتوگو؟</string> + <string name="action_delete_conversation">حذف گفتوگو</string> + <string name="account_date_joined">در %1$s پیوست</string> + <string name="title_login">ورود</string> + <string name="notification_sign_up_format">%1$s ثبتنام کرد</string> + <string name="pref_title_confirm_favourites">نمایش تأیید پیش از برگزیدن</string> + <string name="tusky_compose_post_quicksetting_label">ایجاد فرسته</string> + <string name="tips_push_notification_migration">ورود دوباره به تمامی حسابها برای به کار انداختن پشتیبانی آگاهیهای ارسالی.</string> + <string name="notification_update_description">آگاهیها هنگام ویرایش فرستههایی که با آنها تعامل داشتهاید</string> + <string name="action_unbookmark">برداشن نشانک</string> + <string name="dialog_push_notification_migration_other_accounts">برای اعطای اجازهٔ اشتراک آگاهیهای ارسالی به تاسکی، دوباره به حسابتان وارد شدید. با این حال هنوز حسابهایی دیگر دارید که اینگونه مهاجرت داده نشدهاند. به آنها رفته و برای به کار انداختن پشتیبانی آگاهیهای UnifiedPush یکییکی دوباره وارد شوید.</string> + <string name="action_logout_confirm">مطمئنید که میخواهید از %1$s خارج شوید؟ این کار تمامی دادههای محلی از جمله پیشنویسها و ترجیحات را حذف خواهد کرد.</string> + <string name="duration_14_days">۱۴ روز</string> + <string name="duration_30_days">۳۰ روز</string> + <string name="duration_60_days">۶۰ روز</string> + <string name="duration_90_days">۹۰ روز</string> + <string name="duration_365_days">۳۶۵ روز</string> + <string name="duration_180_days">۱۸۰ روز</string> + <string name="status_count_one_plus">۱+</string> + <string name="dialog_push_notification_migration">تاسکی برای استفاده از آگاهیهای ارسالی با UnifiedPush نیاز به اجازهٔ اشتراک آگاهیها روی کارساز ماستودنتان دارد. این کار نیازمند ورود دوباره برای تغییر حوزههای OAuth اعطایی به تاسکی است. استفاده از گزینهٔ ورود دوباره در اینجا یا در ترجیحات حساب، تمامی انبارهها و پیشنویسهای محلیتان را نگه خواهد داشت.</string> + <string name="error_could_not_load_login_page">نتوانست صفحهٔ ورود را بار کند.</string> + <string name="pref_title_notification_filter_sign_ups">کسی ثبتنام کرد</string> + <string name="notification_update_name">ویرایشهای فرسته</string> + <string name="action_edit_image">ویرایش تصویر</string> + <string name="notification_update_format">%1$s فرستهاش را ویراست</string> + <string name="pref_title_notification_filter_updates">فرستهای که با آن تعامل داشتهام ویرایش شده</string> + <string name="notification_sign_up_name">ثبتنامها</string> + <string name="notification_sign_up_description">آگاهیها دربارهٔ کاربران جدید</string> + <string name="title_migration_relogin">ورود دوباره برای آگاهیهای ارسالی</string> + <string name="action_dismiss">رد کردن</string> + <string name="action_details">جزییات</string> + <string name="saving_draft">ذخیرهٔ پیشنویس…</string> + <string name="error_following_hashtag_format">خطا در پیگیری #%1$s</string> + <string name="error_unfollowing_hashtag_format">خطا در ناپیگیری #%1$s</string> + <string name="delete_scheduled_post_warning">حذف این فرستهٔ زمانبسته؟</string> + <string name="instance_rule_title">قواعد %1$s</string> + <string name="instance_rule_info">با ورودتان، قواعد %1$s را میپذیرید.</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="failed_to_pin">شکست در سنجاق کردن</string> + <string name="failed_to_unpin">شکست در برداشتن سنجاق</string> + <string name="error_multimedia_size_limit">پروندههای صوتی و ویدیویی نمیتوانند بیش از %1$sمب باشند.</string> + <string name="error_image_edit_failed">تصویر نتوانست ویرایش شود.</string> + <string name="description_post_language">زبان فرسته</string> + <string name="pref_show_self_username_always">همیشه</string> + <string name="pref_show_self_username_disambiguate">هنگام ورود چندین حساب</string> + <string name="pref_show_self_username_never">هرگز</string> + <string name="pref_title_show_self_username">نمایش نام کاربری در نوارابزارها</string> + <string name="action_add_reaction">افزودن واکنش</string> + <string name="action_set_focus">تنظیم نقطهٔ تمرکز</string> + <string name="duration_no_change">(بدون تغییر)</string> + <string name="error_loading_account_details">شکست در بار کردن جزییات حساب</string> + <string name="set_focus_description">ضربه زده یا دایره را کشیده تا نقطهٔ کانونیای که همواره باید در بندانگشتیها نمایان باشد را برگزینید.</string> + <string name="compose_save_draft_loses_media">ذخیرهٔ پیشنویس؟ (پیوستها هنگام بازگردانی پیشنویس، دوباره بارگذاری خواهند شد)</string> + <string name="action_add_or_remove_from_list">افزودن یا برداشتن از سیاهه</string> + <string name="failed_to_add_to_list">شکست در افزودن حساب به سیاهه</string> + <string name="failed_to_remove_from_list">شکست در برداشتن حساب از سیاهه</string> + <string name="no_lists">هیچ سیاههای ندارید.</string> + <string name="notification_report_format">گزارش جدید روی %1$s</string> + <string name="notification_header_report_format">%1$s، %2$s را گزارش داد</string> + <string name="notification_summary_report_format">%1$s · %2$d فرسته پیوسته</string> + <string name="error_following_hashtags_unsupported">این نمونه از پیگیری برچسبها پشتیبانی نمیکند.</string> + <string name="title_followed_hashtags">برچسبهای دنبالی</string> + <string name="confirmation_hashtag_unfollowed">پیگیری #%1$s لغو شد</string> + <string name="pref_title_notification_filter_reports">گزارشی جدید موجود است</string> + <string name="notification_report_name">گزارشها</string> + <string name="notification_report_description">آگاهیها دربارهٔ گزارشهای مدیریتی</string> + <string name="report_category_violation">نقض قانون</string> + <string name="report_category_spam">هرزنامه</string> + <string name="report_category_other">دیگر</string> + <string name="action_unfollow_hashtag_format">لغو پیگیری #%1$s؟</string> + <string name="status_created_at_now">اکنون</string> + <string name="hint_media_description_missing">رسانه باید شرحی داشته باشد.</string> + <string name="pref_default_post_language">زبان فرستهٔ پیشگزیده (همگام با کارساز)</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="pref_title_http_proxy_port_message">درگاه باید بین %2$d و %1$d باشد</string> + <string name="description_post_edited">ویراسته</string> + <string name="error_muting_hashtag_format">خطا در خموش کردن #%1$s</string> + <string name="error_unmuting_hashtag_format">خطا در ناخموش کردن #%1$s</string> + <string name="post_media_alt">متن جایگزین</string> + <string name="post_edited">%1$s را ویراست</string> + <string name="error_status_source_load">شکست در بار کردن مبدأ وضعیت از کارساز.</string> + <string name="a11y_label_loading_thread">بار کردن رشته</string> + <string name="title_edits">ویرایشها</string> + <string name="pref_summary_http_proxy_missing"><تنظیم نشده></string> + <string name="pref_summary_http_proxy_disabled">از کار افتاده</string> + <string name="pref_summary_http_proxy_invalid"><نامعتبر></string> + <string name="pref_title_reading_order">ترتیب خواندن</string> + <string name="pref_reading_order_newest_first">نخست جدیدترین</string> + <string name="status_edit_info">ویراسته: %1$s</string> + <string name="status_created_info">ایجاد شده: %1$s</string> + <string name="action_post_failed">بارگذاری شکست خورد</string> + <string name="action_post_failed_show_drafts">نمایش پیشنویسها</string> + <string name="action_post_failed_do_nothing">رد کردن</string> + <string name="action_post_failed_detail">بارگذاری فرستهتان شکست خورد و در پیشنویسها ذخیره شد. +\n +\nارتباط با کارساز برقرار نشد یا فرسته را رد کرد.</string> + <string name="action_post_failed_detail_plural">بارگذاری فرستههایتان شکست خورد و در پیشنویسها ذخیره شد. +\n +\nارتباط با کارساز برقرار نشد یا فرستهها را رد کرد.</string> + <string name="description_login">در بیشتر حالتها کار میکند. هیچ دادهای به دیگر کارهها نشت نمیکند.</string> + <string name="pref_reading_order_oldest_first">نخست قدیمیترین</string> + <string name="description_browser_login">ممکن است از روشهای تأیید خویت اضافی پشتیبانی کند؛ ولی نیازمند مرورگری پشتیبانی شده است.</string> + <string name="action_discard">دور انداختن تغییرات</string> + <string name="action_continue_edit">ادامهٔ ویرایش</string> + <string name="action_browser_login">ورود با مرورگر</string> + <string name="compose_unsaved_changes">تغییراتی ذخیره نشده دارید.</string> + <string name="action_share_account_link">همرسانی پیوند به حساب</string> + <string name="action_share_account_username">همرسانی نام کاربری حساب</string> + <string name="send_account_link_to">همرسانی نشانی حساب به…</string> + <string name="send_account_username_to">همرسانی نام کاربری حساب به…</string> + <string name="account_username_copied">نام کاربری رونوشت شد</string> + <string name="mute_notifications_switch">خموشی آگاهیها</string> + <string name="title_public_trending_hashtags">برچسبهای داغ</string> + <string name="accessibility_talking_about_tag">%1$d نفر دارند دربارهٔ برچسب %2$s حرف میزنند</string> + <string name="total_usage">استفادهٔ کل</string> + <string name="total_accounts">مجموع حسابها</string> + <string name="dialog_follow_hashtag_title">پیگیری برچسب</string> + <string name="dialog_follow_hashtag_hint">#برچسب</string> + <string name="action_refresh">نوسازی</string> + <string name="notification_unknown_name">ناشناخته</string> + <string name="socket_timeout_exception">تماس با کارسازتان بیش از حد طول کشید</string> + <string name="ui_error_unknown">دلیل نامعلوم</string> + <string name="ui_error_bookmark">نشانک گذاری فرسته شکست خورد: %1$s</string> + <string name="ui_error_clear_notifications">پاک سازی آگاهیها شکست خورد: %1$s</string> + <string name="ui_error_favourite">برگزیدن فرسته شکست خورد: %1$s</string> + <string name="ui_error_vote">رأی دادن در نظرسنجی شکست خورد: %1$s</string> + <string name="ui_error_accept_follow_request">پذیرش درخواست پیگیری شکست خورد: %1$s</string> + <string name="ui_success_accepted_follow_request">درخواست پیگیری پذیرفته شد</string> + <string name="ui_success_rejected_follow_request">درخواست پیگیری مسدود شد</string> + <string name="ui_error_reblog">تقویت فرسته شکست خورد: %1$s</string> + <string name="ui_error_reject_follow_request">رد کردن درخواست پیگیری شکست خورد: %1$s</string> + <string name="status_filtered_show_anyway">نمایش به هر روی</string> + <string name="status_filter_placeholder_label_format">پالوده: %1$s</string> + <string name="pref_title_account_filter_keywords">نمایهها</string> + <string name="hint_filter_title">پالایهام</string> + <string name="label_filter_title">عنوان</string> + <string name="filter_action_warn">هشدار</string> + <string name="filter_action_hide">نهفتن</string> + <string name="filter_description_warn">نهفتن با هشدار</string> + <string name="filter_description_hide">نهفتن کامل</string> + <string name="label_filter_action">کنش پالایه</string> + <string name="label_filter_context">بافتارهای پالایه</string> + <string name="label_filter_keywords">کلیدواژگان یا عبارتها برای پالایش</string> + <string name="action_add">افزودن</string> + <string name="filter_keyword_display_format">%1$s (واژهٔ کامل)</string> + <string name="filter_keyword_addition_title">افزودن کلیدواژه</string> + <string name="filter_edit_keyword_title">ویرایش کلیدواژه</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="pref_title_show_stat_inline">نمایش آمار فرستهها در خط زمانی</string> + <string name="help_empty_home">این <b>خط زمانی خانگیتان</b> است. فرستههای جدید را از حسابهایی که پیمیگیرید نشان میدهد. +\n +\nبرای کاوش حسابها میتوانید آنها را در دیگر خطهای زمانی (برای نمونه خط زمانی محلّی نمونهتان [iconics gmd_group]) یافته یا با نام (برای نمونه Tusky برای حساب ماستودونمان) بجوییدشان [iconics gmd_search].</string> + <string name="post_media_image">تصویر</string> + <string name="select_list_manage">مدیریت سیاههها</string> + <string name="load_newest_notifications">بار کردن جدیدترین آگاهیها</string> + <string name="compose_delete_draft">حذف پیشنویس؟</string> + <string name="error_missing_edits">کارسازتان میداند که این فرسته ویرایش شده؛ ولی رونوشتی از ویرایشها ندارد. پس نمیتوانند نشانتان داده شوند. +\n +\nاین <a href="https://github.com/mastodon/mastodon/issues/25398">مسئلهٔ ماستودون</a> را ببینید.</string> + <string name="pref_ui_text_size">اندازهٔ متن میانای کاربری</string> + <string name="notification_listenable_worker_name">فعّالیت پسزمینه</string> + <string name="notification_listenable_worker_description">آگاهیها هنگامی که تاسکی در پسزمینه کار میکند</string> + <string name="notification_notification_worker">واکشی آگاهیها…</string> + <string name="notification_prune_cache">نگهداری انباره…</string> + <string name="error_media_upload_sending_fmt">بارگذاری شکست خورد: %1$s</string> + <string name="about_account_info_title">حسابتان</string> + <string name="about_device_info_title">افزارهتان</string> + <string name="about_device_info">%1$s %2$s +\nنگارش اندروید: %3$s +\nنگارش SDK: %4$d</string> + <string name="about_account_info">@%1$s@%2$s +\nنگارش: %3$s</string> + <string name="about_copy">رونوشت از اطّلاعات افزاره و نگارش</string> + <string name="about_copied">اطّلاعات افزاره و نگارش رونوشت شد</string> + <string name="list_exclusive_label">نهفتن از خط زمانی خانگی</string> + <string name="error_media_playback">پخش شکست خورد: %1$s</string> + <string name="dialog_delete_filter_positive_action">حذف</string> + <string name="dialog_delete_filter_text">حذف پالایهٔ «%1$s»؟</string> + <string name="dialog_save_profile_changes_message">میخواهید تغییرات نمایهتان را ذخیره کنید؟</string> + <string name="action_view_filter">دیدن پالایه</string> + <string name="muting_hashtag_success_format">خموش کردن برچسب #%1$s به عنوان هشدار</string> + <string name="unmuting_hashtag_success_format">ناخموش کردن برچسب #%1$s</string> + <string name="following_hashtag_success_format">اکنون برچسب #%1$s دنبال میشود</string> + <string name="unfollowing_hashtag_success_format">دیگر برچسب #%1$s دنبال نمیشود</string> + <string name="help_empty_conversations"><b>پیامهای خصوصیتان</b> که گاهی گفتوگوها یا پیامهای مستقیم نامیده میشوند، اینجایند. +\n +\nپیامهای خصوصی با تنظیم نمایانی [iconics gmd_public] فرسته به [iconics gmd_mail] <i>مستقیم</i> و نام بردن از کاربر یا کاربرانی در متن ایجاد میشود. +\n +\nبرای نمونه میتوانید در نمای نمایهٔ حسابی روی دکمهٔ ایجاد [iconics gmd_edit] زده و نمایانی را تغییر دهید. \u0020</string> + <string name="help_empty_lists">این <b>نمایهٔ سیاههتان</b> است. میتوانید تعدادی سیاههٔ خصوصی ایحاد کرده و حسابها را بدان بیفزایید. +\n +\n به خاطر داشته باشید که تنها میتوانید افرادی را که پی میگیرید به سیاهههایتان بیفزایید. +\n +\n این سیاههها میتوانند به عنوان زبانهای در زبانههای ترجیحات حساب [iconics gmd_account_circle] [iconics gmd_navigate_next] استفاده شوند. \u0020</string> + <string name="error_blocking_domain">شکست در خموش کردن %1$s: %2$s</string> + <string name="error_unblocking_domain">شکست در ناخموش کردن %1$s: %2$s</string> + <string name="label_image">تصویر</string> + <string name="app_theme_system_black">استفاده از طرّاحی سامانه (سیاه)</string> + <string name="title_public_trending_statuses">فرستههای داغ</string> + <string name="list_reply_policy_none">هیچکس</string> + <string name="list_reply_policy_list">اعضای سیاهه</string> + <string name="list_reply_policy_followed">هر کاربر پیگرفته</string> + <string name="list_reply_policy_label">نمایش پاسخها به</string> + <string name="pref_title_show_self_boosts">نمایش خودتقویتها</string> + <string name="pref_title_show_self_boosts_description">کسی فرستهٔ خودش را تقویت میکند</string> + <string name="reply_sending">فرستادن…</string> + <string name="reply_sending_long">پاسختان دارد فرستاده میشود.</string> + <string name="action_translate">ترجمه</string> + <string name="action_show_original">نمایش اصلی</string> + <string name="report_category_legal">حقوقی</string> + <string name="unknown_notification_type">گونهٔ آگاهی ناشناخته</string> + <string name="label_translating">ترجمه کردن…</string> + <string name="label_translated">ترجمه شده از %1$s با %2$s</string> + <string name="ui_error_translate">نتوانست ترجمه کند: %1$s</string> + <string name="pref_title_per_timeline_preferences">ترجیحات بر مبنای خط زمانی</string> + <string name="pref_title_show_notifications_filter">نمایش پالایهٔ آگاهیها</string> + <string name="dialog_follow_warning">پیگیری این حساب؟</string> + <string name="pref_title_confirm_follows">نمایش تأیید پیش از پیگیری</string> + <string name="url_copied">نشانی رونوشت شد</string> + <string name="confirmation_hashtag_copied">«%1$s» رونوشت شد</string> + <string name="pref_default_reply_privacy">محرمانگی پیشگزیدهٔ پاسخ (همگام نشده با کارساز)</string> + <string name="error_deleting_filter">خطا در حذف پالایهٔ «%1$s»</string> + <string name="error_saving_filter">خطا در ذخیرهٔ پالایهٔ «%1$s»</string> + <string name="action_follow_hashtag">پیگیری برچسبی جدید</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml new file mode 100644 index 0000000..8b84a86 --- /dev/null +++ b/app/src/main/res/values-fi/strings.xml @@ -0,0 +1,308 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="pref_title_animate_custom_emojis">Animoi mukautetut emojit</string> + <string name="pref_title_animate_gif_avatars">Animoi GIF-avatarit</string> + <string name="app_theme_system">Seuraa laitteen teemaa</string> + <string name="dialog_unfollow_warning">Lopeta tilin seuraaminen\?</string> + <string name="dialog_delete_post_warning">Poista tuuttaus\?</string> + <string name="link_whats_an_instance">Mikä on instanssi\?</string> + <string name="action_copy_link">Kopioi linkki</string> + <string name="action_open_in_web">Avaa selaimessa</string> + <string name="action_login">Kirjaudu Mastodonilla</string> + <string name="title_edit_profile">Muokkaa profiilia</string> + <string name="draft_deleted">Luonnos poistettu</string> + <string name="poll_new_choice_hint">Vaihtoehto %1$d</string> + <string name="poll_allow_multiple_choices">Monivalinta</string> + <string name="add_poll_choice">Lisää vaihtoehto</string> + <string name="duration_7_days">7 päivää</string> + <string name="duration_3_days">3 päivää</string> + <string name="duration_1_day">1 päivä</string> + <string name="duration_6_hours">6 tuntia</string> + <string name="duration_1_hour">1 tunti</string> + <string name="duration_30_min">30 minuuttia</string> + <string name="duration_5_min">5 minuuttia</string> + <string name="add_hashtag_title">Lisää aihetunniste</string> + <string name="description_post_media_no_description_placeholder">Ei kuvausta</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="download_failed">Lataus epäonnistui</string> + <string name="action_open_post">Avaa tuuttaus</string> + <string name="system_default">Järjestelmän oletus</string> + <string name="emoji_style">Emojien tyyli</string> + <string name="send_post_notification_title">Lähetetään tuuttausta…</string> + <string name="compose_save_draft">Tallennetaanko luonnoksena\?</string> + <string name="lock_account_label">Lukitse tili</string> + <string name="add_account_name">Lisää tili</string> + <string name="follows_you">Seuraa sinua</string> + <string name="about_tusky_account">Tuskyn profiili</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="description_account_locked">Lukittu tili</string> + <string name="notification_subscription_name">Uudet tuuttaukset</string> + <string name="notification_follow_request_name">Seuraamispyynnöt</string> + <string name="notification_follow_name">Uudet seuraajat</string> + <string name="notification_mention_name">Uudet maininnat</string> + <string name="pref_title_http_proxy_settings">HTTP-välityspalvelin</string> + <string name="pref_title_show_replies">Näytä vastaukset</string> + <string name="pref_title_app_theme">Teema</string> + <string name="search_no_results">Ei tuloksia</string> + <string name="hint_content_warning">Sisältövaroitus</string> + <string name="hint_compose">Mitä tapahtuu\?</string> + <string name="hint_domain">Mikä instanssi\?</string> + <string name="downloading_media">Ladataan mediaa</string> + <string name="download_media">Lataa media</string> + <string name="action_open_faved_by">Näytä suosikit</string> + <string name="action_add_tab">Lisää välilehti</string> + <string name="action_schedule_post">Ajasta tuuttaus</string> + <string name="action_emoji_keyboard">Emoji-näppäimistö</string> + <string name="action_content_warning">Sisältövaroitus</string> + <string name="action_access_scheduled_posts">Ajastetut tuuttaukset</string> + <string name="action_edit_profile">Muokkaa profiilia</string> + <string name="action_hide_media">Piilota media</string> + <string name="action_photo_take">Ota kuva</string> + <string name="action_add_poll">Lisää kysely</string> + <string name="action_add_media">Lisää media</string> + <string name="action_view_follow_requests">Seuraamispyynnöt</string> + <string name="action_view_blocks">Estetyt tilit</string> + <string name="action_view_account_preferences">Tiliasetukset</string> + <string name="action_logout">Kirjaudu ulos</string> + <string name="post_content_warning_show_less">Näytä vähemmän</string> + <string name="post_content_warning_show_more">Näytä lisää</string> + <string name="post_media_hidden_title">Media piilotettu</string> + <string name="title_follow_requests">Seuraamispyynnöt</string> + <string name="title_blocks">Estetyt tilit</string> + <string name="title_mutes">Mykistetyt tilit</string> + <string name="title_direct_messages">Viestit</string> + <string name="account_note_saved">Tallennettu!</string> + <string name="edit_poll">Muokkaa</string> + <string name="create_poll_title">Kysely</string> + <string name="title_accounts">Tilit</string> + <string name="button_done">Valmis</string> + <string name="button_back">Takaisin</string> + <string name="button_continue">Jatka</string> + <string name="poll_vote">Äänestä</string> + <string name="poll_info_closed">suljettu</string> + <string name="notifications_apply_filter">Suodatin</string> + <string name="list">Lista</string> + <string name="hashtags">Aihetunnisteet</string> + <string name="description_visibility_public">Julkinen</string> + <string name="pin_action">Kiinnitä</string> + <string name="unpin_action">Poista kiinnitys</string> + <string name="profile_badge_bot_text">Botti</string> + <string name="action_remove">Poista</string> + <string name="title_lists">Listat</string> + <string name="action_lists">Listat</string> + <string name="filter_dialog_update_button">Päivitä</string> + <string name="filter_dialog_remove_button">Poista</string> + <string name="post_media_audio">Ääni</string> + <string name="post_media_video">Video</string> + <string name="post_media_images">Kuvat</string> + <string name="about_title_activity">Tietoja</string> + <string name="pref_title_proxy_settings">Välityspalvelin</string> + <string name="post_privacy_followers_only">Vain seuraajat</string> + <string name="post_privacy_public">Julkinen</string> + <string name="pref_title_post_tabs">Välilehdet</string> + <string name="pref_title_language">Kieli</string> + <string name="pref_title_browser_settings">Selain</string> + <string name="app_theme_black">Musta</string> + <string name="app_theme_light">Vaalea</string> + <string name="app_them_dark">Tumma</string> + <string name="pref_title_timeline_filters">Suodattimet</string> + <string name="pref_title_timelines">Aikajanat</string> + <string name="pref_title_notification_filter_follows">seurasi</string> + <string name="pref_title_notification_filter_mentions">mainitsi</string> + <string name="pref_title_notifications_enabled">Ilmoitukset</string> + <string name="pref_title_edit_notification_settings">Ilmoitukset</string> + <string name="dialog_download_image">Lataa</string> + <string name="label_avatar">Profiilikuva</string> + <string name="label_quick_reply">Vastaa…</string> + <string name="hint_search">Hae…</string> + <string name="hint_note">Kuvaus</string> + <string name="title_links_dialog">Linkit</string> + <string name="title_mentions_dialog">Maininnat</string> + <string name="title_hashtags_dialog">Aihetunnisteet</string> + <string name="action_hashtags">Aihetunnisteet</string> + <string name="action_mentions">Maininnat</string> + <string name="action_links">Linkit</string> + <string name="action_reset_schedule">Nollaa</string> + <string name="action_access_drafts">Luonnokset</string> + <string name="action_search">Hae</string> + <string name="action_reject">Hylkää</string> + <string name="action_accept">Hyväksy</string> + <string name="action_undo">Peruuta</string> + <string name="action_edit_own_profile">Muokkaa</string> + <string name="action_save">Tallenna</string> + <string name="action_mention">Mainitse</string> + <string name="action_unmute">Poista mykistys</string> + <string name="action_mute">Mykistä</string> + <string name="action_share">Jaa</string> + <string name="action_view_media">Media</string> + <string name="action_view_bookmarks">Kirjanmerkit</string> + <string name="action_view_favourites">Suosikit</string> + <string name="action_view_preferences">Asetukset</string> + <string name="action_view_profile">Profiili</string> + <string name="action_close">Sulje</string> + <string name="action_retry">Yritä uudelleen</string> + <string name="action_send">TUUT</string> + <string name="action_delete">Poista</string> + <string name="action_edit">Muokkaa</string> + <string name="action_report">Ilmianna</string> + <string name="action_unblock">Poista esto</string> + <string name="action_block">Estä</string> + <string name="action_unfollow">Seurataan</string> + <string name="action_follow">Seuraa</string> + <string name="action_send_public">TUUT!</string> + <string name="title_view_thread">Lanka</string> + <string name="title_scheduled_posts">Ajastetut tuuttaukset</string> + <string name="action_reply">Vastaa</string> + <string name="post_username_format">\@%1$s</string> + <string name="title_licenses">Lisenssit</string> + <string name="title_drafts">Luonnokset</string> + <string name="title_favourites">Suosikit</string> + <string name="title_bookmarks">Kirjanmerkit</string> + <string name="title_followers">Seuraajat</string> + <string name="title_follows">Seurataan</string> + <string name="title_posts_pinned">Kiinnitetty</string> + <string name="title_posts">Julkaisut</string> + <string name="title_tab_preferences">Välilehdet</string> + <string name="title_public_local">Paikallinen</string> + <string name="title_notifications">Ilmoitukset</string> + <string name="title_home">Koti</string> + <string name="action_unfavourite">Poista suosikki</string> + <string name="action_open_reblogger">Avaa jakajan tili</string> + <string name="action_edit_image">Muokkaa kuvaa</string> + <string name="action_view_mutes">Mykistetyt tilit</string> + <string name="notification_follow_format">%1$s seurasi sinua</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d uusi kannsakäyminen</item> + <item quantity="other">%1$d uutta kanssakäymistä</item> + </plurals> + <string name="action_delete_and_redraft">Poista ja kirjoita uudelleen</string> + <string name="error_media_upload_image_or_video">Samaan julkaisuun ei voi liittää sekä kuvia että videoita.</string> + <string name="notification_summary_medium">%1$s, %2$s, ja %3$s</string> + <string name="action_delete_list">Poista lista</string> + <string name="action_rename_list">Muuta listan nimeä</string> + <string name="error_media_upload_type">Tällaista tiedostoa ei voida ladata ylös.</string> + <string name="notification_follow_request_format">%1$s haluaa seurata sinua</string> + <string name="title_public_federated">Verkostoitu</string> + <string name="error_media_upload_sending">Lähettäminen epäonnistui.</string> + <string name="report_username_format">Ilmianna @%1$s</string> + <string name="report_comment_hint">Lisähuomautuksia\?</string> + <string name="error_loading_account_details">Tilitietojen lataaminen epäonnistui</string> + <string name="error_media_upload_permission">Tiedostojen lähettäminen vaatii lukuoikeuden.</string> + <string name="action_set_caption">Aseta kuvaus</string> + <string name="action_bookmark">Kirjanmerkki</string> + <string name="action_unmute_domain">Poista mykistys verkkonimeltä %1$s</string> + <string name="action_open_as">Avaa tilinä %1$s</string> + <string name="action_more">Enemmän</string> + <string name="notification_mention_format">%1$s on maininnut sinut</string> + <string name="error_media_upload_opening">Tiedoston lataaminen epäonnistui.</string> + <string name="notification_reblog_format">%1$s jakoi julkaisusi</string> + <string name="error_compose_character_limit">Julkaisusi on liian pitkä!</string> + <string name="action_unmute_conversation">Poista keskustelun mykistys</string> + <string name="download_image">Ladataan kuvaa %1$s</string> + <string name="action_mute_conversation">Mykistä keskustelu</string> + <string name="error_generic">Tapahtui virhe.</string> + <string name="action_logout_confirm">Halutako varmasti kirjautua ulos tililtä %1$s1\?</string> + <string name="action_unreblog">Poista jako</string> + <string name="action_create_list">Luo lista</string> + <string name="action_mute_domain">Mykistä %1$s</string> + <string name="action_add_to_list">Lisää tili listalle</string> + <string name="action_view_domain_mutes">Piilotetut verkkonimet</string> + <string name="notification_favourite_format">%1$s lisäsi julkaisusi suosikkeihinsa</string> + <string name="action_open_reblogged_by">Näytä jaot</string> + <string name="action_show_reblogs">Näytä jaetut julkaisut</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s ja %4$d muuta</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Kuvaa näkövammaisille +\n(enintään %1$d merkkiä)</item> + <item quantity="other"/> + </plurals> + <string name="edit_hashtag_hint">Aihetunniste ilman #-merkkiä</string> + <string name="title_domain_mutes">Piilotetus verkkonimet</string> + <string name="action_share_as">Jaa…</string> + <string name="error_no_web_browser_found">Verkkoselainta ei löytynyt.</string> + <string name="action_delete_conversation">Poista keskustelu</string> + <string name="action_remove_from_list">Poista tili listalta</string> + <string name="notification_subscription_format">%1$s julkaisi juuri</string> + <string name="action_quick_reply">Vastaa nopeasti</string> + <string name="action_hide_reblogs">Piilota jaetut julkaisut</string> + <string name="footer_empty">Täällä ei ole mitään. Liu\'uta alaspäin päivittääksesi!</string> + <string name="error_network">Verkkovirhe! Tarkista yhteytesi ja yritä uudelleen!</string> + <string name="action_open_media_n">Avaa media No. %1$d</string> + <string name="error_sender_account_gone">Julkaisun lähettäminen epäonnistui.</string> + <string name="error_empty">Tätä kenttää ei voi jättää tyhjäksi.</string> + <string name="title_announcements">Tiedotukset</string> + <string name="notification_subscription_description">Ilmoitukset seuraamiesi uusista julkaisuista</string> + <string name="action_favourite">Lisää suosikiksi</string> + <string name="error_media_download_permission">Tiedostojen tallentamiseen vaaditaan kirjoitusoikeus.</string> + <string name="notification_summary_small">%1$s ja %2$s</string> + <string name="error_invalid_domain">Syöttämäsi verkkonimi on virheellinen</string> + <string name="action_unmute_desc">Poista mykistys tililtä %1$s</string> + <string name="action_compose">Kirjoita julkaisu</string> + <string name="hint_search_people_list">Hae seuraamiasi henkilöitä</string> + <string name="message_empty">Täällä ei ole mitään.</string> + <string name="notification_sign_up_format">%1$s rekisteröityi</string> + <string name="notification_sign_up_name">Rekisteröitymiset</string> + <string name="notification_sign_up_description">Ilmoitukset uusista käyttäkistä</string> + <string name="title_login">Sisäänkirjautuminen</string> + <string name="action_unbookmark">Poista kirjanmerkki</string> + <string name="duration_90_days">90 päivää</string> + <string name="duration_180_days">180 päivää</string> + <string name="duration_365_days">365 päivää</string> + <string name="error_could_not_load_login_page">Sisäänkirjautumissivun lataaminen epäonnistui.</string> + <string name="action_reblog">Jaa</string> + <string name="action_toggle_visibility">Julkaisun näkyvyys</string> + <string name="title_posts_with_replies">Vastauksetkin</string> + <string name="post_boosted_format">%1$s jakoi</string> + <string name="post_sensitive_media_title">Herkkää sisältoä</string> + <string name="post_sensitive_media_directions">Klikkaa näyttääksesi</string> + <string name="post_content_show_more">Laajenna</string> + <string name="post_content_show_less">Vähennä</string> + <string name="send_post_link_to">Jaa julkaisun URL…</string> + <string name="notification_update_format">%1$s muokkasi julkaisua</string> + <string name="notification_update_name">Julkaisujen muokkaukset</string> + <string name="title_migration_relogin">Kirjaudu uudestaan sisään ottaaksesi vastaan push-ilmoituksia</string> + <string name="action_details">Yksityiskohdat</string> + <string name="error_image_edit_failed">Kuvaa ei voitu muokata.</string> + <string name="description_visibility_unlisted">Listaamaton</string> + <string name="dialog_delete_conversation_warning">Poista tämä keskustelu\?</string> + <string name="pref_title_show_media_preview">Lataa median esikatselu</string> + <string name="post_privacy_unlisted">Listaamaton</string> + <string name="pref_title_alway_show_sensitive_media">Näytä aina arkaluonteinen sisältö</string> + <string name="send_post_notification_cancel_title">Lähettäminen peruutettu</string> + <string name="description_visibility_private">Seuraajat</string> + <string name="pref_show_self_username_always">Aina</string> + <string name="pref_show_self_username_never">Ei koskaan</string> + <string name="notification_boost_name">Buustaukset</string> + <string name="notification_favourite_name">Suosikit</string> + <string name="notification_poll_name">Äänestykset</string> + <string name="notification_poll_description">Ilmoitukset päättyneistä äänestyksistä</string> + <string name="filter_edit_title">Muokkaa suodatinta</string> + <string name="action_add_reaction">lisää reaktio</string> + <string name="confirmation_reported">Lähetetty!</string> + <string name="login_connection">Yhdistetään…</string> + <string name="dialog_mute_warning">Hiljennä @%1$s\?</string> + <string name="dialog_mute_hide_notifications">Piilota ilmoitukset</string> + <string name="pref_title_appearance_settings">Ulkoasu</string> + <string name="pref_title_show_boosts">Näytä buustaukset</string> + <string name="pref_default_media_sensitivity">Merkitse media aina arkaluontoiseksi</string> + <string name="pref_failed_to_sync">Asetusten synkronointi epäonnistui</string> + <string name="post_text_size_smallest">Pienin</string> + <string name="post_text_size_small">Pieni</string> + <string name="post_text_size_medium">Keskikokoinen</string> + <string name="post_text_size_large">Suuri</string> + <string name="post_text_size_largest">Suurin</string> + <string name="post_share_link">Jaa linkki postaukseen</string> + <string name="post_media_attachments">Liitteet</string> + <string name="title_media">Media</string> + <string name="load_more_placeholder_text">lataa lisää</string> + <string name="pref_title_public_filter_keywords">Julkiset aikajanat</string> + <string name="pref_title_thread_filter_keywords">Keskustelut</string> + <string name="filter_addition_title">Lisää suodatin</string> + <string name="later">Myöhemmin</string> + <string name="description_post_cw">Sisältövaroitus: %1$s</string> + <string name="pref_title_http_proxy_server">HTTP-välityspalvelin</string> + <string name="restart">Käynnistä uudelleen</string> + <string name="error_failed_app_registration">Tunnistautuminen valitsemasi instanssin kanssa epäonnistui.</string> + <string name="dialog_block_warning">Estä @%1$s\?</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-fr-rBE/strings.xml b/app/src/main/res/values-fr-rBE/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/main/res/values-fr-rBE/strings.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources></resources> \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000..62ec43d --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,691 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Une erreur s’est produite.</string> + <string name="error_network">Une erreur réseau s’est produite. Veuillez vérifier votre connexion puis réessayez.</string> + <string name="error_empty">Ce champ ne peut pas être vide.</string> + <string name="error_invalid_domain">Le domaine saisi est invalide</string> + <string name="error_failed_app_registration">Échec d’authentification auprès de l’instance. Si cela continue à se produire, essayez Se connecter avec le navigateur depuis le menu.</string> + <string name="error_no_web_browser_found">Impossible de trouver un navigateur web à utiliser.</string> + <string name="error_authorization_unknown">Une erreur d’autorisation inconnue s’est produite. Si cela continue à se produire, essayez Se connecter avec le navigateur depuis le menu.</string> + <string name="error_authorization_denied">Authentification refusée. Si vous êtes sur·e d\'avoir entré les bons identifiants, essayez Se connecter avec le navigateur depuis le menu.</string> + <string name="error_retrieving_oauth_token">Impossible de récupérer le jeton d’authentification. Si cela continue à se produire, essayez Se connecter avec le navigateur depuis le menu.</string> + <string name="error_compose_character_limit">Votre message est trop long !</string> + <string name="error_media_upload_type">Ce type de fichier ne peut pas être téléversé.</string> + <string name="error_media_upload_opening">Le fichier ne peut pas être ouvert.</string> + <string name="error_media_upload_permission">Permission requise pour lire le média.</string> + <string name="error_media_download_permission">Permission requise pour enregistrer le média.</string> + <string name="error_media_upload_image_or_video">Un même message ne peut contenir à la fois une vidéo et une image.</string> + <string name="error_media_upload_sending">Échec d’envoi du média.</string> + <string name="error_sender_account_gone">Erreur lors de l’envoi du message.</string> + <string name="title_home">Accueil</string> + <string name="title_notifications">Notifications</string> + <string name="title_public_local">Local</string> + <string name="title_public_federated">Global</string> + <string name="title_direct_messages">Messages directs</string> + <string name="title_tab_preferences">Onglets</string> + <string name="title_view_thread">Fil</string> + <string name="title_posts">Messages</string> + <string name="title_posts_with_replies">Avec réponses</string> + <string name="title_posts_pinned">Épinglés</string> + <string name="title_follows">Abonnements</string> + <string name="title_followers">Abonné·e·s</string> + <string name="title_favourites">Favoris</string> + <string name="title_mutes">Comptes masqués</string> + <string name="title_blocks">Comptes bloqués</string> + <string name="title_follow_requests">Demandes d’abonnement</string> + <string name="title_edit_profile">Modifier votre profil</string> + <string name="title_drafts">Brouillons</string> + <string name="title_licenses">Licences</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s a partagé</string> + <string name="post_sensitive_media_title">Contenu sensible</string> + <string name="post_media_hidden_title">Média caché</string> + <string name="post_sensitive_media_directions">Appuyer pour voir</string> + <string name="post_content_warning_show_more">Voir plus</string> + <string name="post_content_warning_show_less">Voir moins</string> + <string name="post_content_show_more">Déplier</string> + <string name="post_content_show_less">Replier</string> + <string name="message_empty">Rien ici.</string> + <string name="footer_empty">Il n’y a aucun pouet ici pour l’instant. Glissez vers le bas pour actualiser !</string> + <string name="notification_reblog_format">%1$s a partagé votre message</string> + <string name="notification_favourite_format">%1$s a ajouté votre message à ses favoris</string> + <string name="notification_follow_format">%1$s vous suit</string> + <string name="report_username_format">Signaler @%1$s</string> + <string name="report_comment_hint">Commentaires additionnels \?</string> + <string name="action_quick_reply">Réponse rapide</string> + <string name="action_reply">Répondre</string> + <string name="action_reblog">Partager</string> + <string name="action_unreblog">Annuler le partage</string> + <string name="action_favourite">Favori</string> + <string name="action_unfavourite">Supprimer le favori</string> + <string name="action_more">Plus</string> + <string name="action_compose">Écrire</string> + <string name="action_login">Se connecter avec Tusky</string> + <string name="action_logout">Déconnexion</string> + <string name="action_logout_confirm">Êtes-vous certain·e· de vouloir déconnecter le compte %1$s ?</string> + <string name="action_follow">Suivre</string> + <string name="action_unfollow">Ne plus suivre</string> + <string name="action_block">Bloquer</string> + <string name="action_unblock">Débloquer</string> + <string name="action_hide_reblogs">Cacher les partages</string> + <string name="action_show_reblogs">Afficher les partages</string> + <string name="action_report">Signaler</string> + <string name="action_delete">Supprimer</string> + <string name="action_send">POUET</string> + <string name="action_send_public">POUET !</string> + <string name="action_retry">Réessayer</string> + <string name="action_close">Fermer</string> + <string name="action_view_profile">Profil</string> + <string name="action_view_preferences">Préférences</string> + <string name="action_view_account_preferences">Préférences de compte</string> + <string name="action_view_favourites">Favoris</string> + <string name="action_view_mutes">Comptes masqués</string> + <string name="action_view_blocks">Comptes bloqués</string> + <string name="action_view_follow_requests">Demandes d’abonnement</string> + <string name="action_view_media">Média</string> + <string name="action_open_in_web">Ouvrir dans votre navigateur</string> + <string name="action_add_media">Ajouter un média</string> + <string name="action_photo_take">Prendre une photo</string> + <string name="action_share">Partager</string> + <string name="action_mute">Masquer</string> + <string name="action_unmute">Ne plus masquer</string> + <string name="action_mention">Mentionner</string> + <string name="action_hide_media">Cacher les médias</string> + <string name="action_open_drawer">Ouvrir le menu</string> + <string name="action_save">Enregistrer</string> + <string name="action_edit_profile">Modifier le profil</string> + <string name="action_edit_own_profile">Modifier</string> + <string name="action_undo">Annuler</string> + <string name="action_accept">Accepter</string> + <string name="action_reject">Refuser</string> + <string name="action_search">Rechercher</string> + <string name="action_access_drafts">Brouillons</string> + <string name="action_toggle_visibility">Visibilité du message</string> + <string name="action_content_warning">Contenu sensible</string> + <string name="action_emoji_keyboard">Clavier d’émojis</string> + <string name="action_add_tab">Ajouter un onglet</string> + <string name="action_links">Liens</string> + <string name="action_mentions">Mentions</string> + <string name="action_hashtags">Hashtags</string> + <string name="action_open_reblogger">Afficher l’auteur·rice du partage</string> + <string name="action_open_reblogged_by">Afficher les partages</string> + <string name="action_open_faved_by">Montrer les favoris</string> + <string name="title_hashtags_dialog">Hashtags</string> + <string name="title_mentions_dialog">Mentions</string> + <string name="title_links_dialog">Liens</string> + <string name="action_open_media_n">Ouvrir le média #%1$d</string> + <string name="download_image">Téléchargement de %1$s</string> + <string name="action_copy_link">Copier le lien</string> + <string name="action_open_as">Ouvrir comme %1$s</string> + <string name="action_share_as">Partager comme …</string> + <string name="download_media">Télécharger le média</string> + <string name="downloading_media">Téléchargement du média</string> + <string name="send_post_link_to">Partager le lien du message avec…</string> + <string name="send_post_content_to">Partager le mesage avec…</string> + <string name="send_media_to">Partager l’image avec …</string> + <string name="confirmation_reported">Envoyé !</string> + <string name="confirmation_unblocked">Le compte est débloqué</string> + <string name="confirmation_unmuted">Le compte n’est plus masqué</string> + <string name="hint_domain">Quelle instance ?</string> + <string name="hint_compose">Quoi de neuf ?</string> + <string name="hint_content_warning">Contenu sensible</string> + <string name="hint_display_name">Nom public</string> + <string name="hint_note">Présentation</string> + <string name="hint_search">Rechercher…</string> + <string name="search_no_results">Aucun résultat</string> + <string name="label_quick_reply">Répondre…</string> + <string name="label_avatar">Image de profil</string> + <string name="label_header">Image d’en-tête</string> + <string name="link_whats_an_instance">Qu’est-ce qu’une instance ?</string> + <string name="login_connection">Connexion en cours…</string> + <string name="dialog_whats_an_instance">Indiquer ici l’adresse ou le domaine d’une instance, + comme mastodon.social, icosahedron.website, social.tchncs.de, + <a href="https://instances.social">et bien d’autres encore</a> (en anglais) ! + \n\nSi vous ne disposez d’aucun compte, vous pouvez renseigner le nom de l’instance que vous souhaitez rejoindre + et y créer un compte.\n\nUne instance est l’endroit où votre compte est + hébergé, mais vous pouvez facilement suivre des personnes d’autres instances et communiquer avec elles + comme si vous étiez sur le même site. + \n\nPour plus d’informations, consultez <a href="https://joinmastodon.org">joinmastodon.org</a>. + </string> + <string name="dialog_title_finishing_media_upload">Mise en ligne des médias</string> + <string name="dialog_message_uploading_media">Téléversement en cours…</string> + <string name="dialog_download_image">Télécharger</string> + <string name="dialog_message_cancel_follow_request">Révoquer la demande d’abonnement ?</string> + <string name="dialog_unfollow_warning">Ne plus suivre ce compte ?</string> + <string name="dialog_delete_post_warning">Supprimer ce message \?</string> + <string name="visibility_public">Public : afficher dans les fils publics</string> + <string name="visibility_unlisted">Non listé : ne pas afficher dans les fils publics</string> + <string name="visibility_private">Abonné·e·s uniquement : seul·e·s vos abonné·e·s verront vos statuts</string> + <string name="visibility_direct">Direct : n’envoyer qu’aux personnes mentionnées</string> + <string name="pref_title_edit_notification_settings">Notifications</string> + <string name="pref_title_notifications_enabled">Notifications</string> + <string name="pref_title_notification_alerts">Alertes</string> + <string name="pref_title_notification_alert_sound">Notifier avec un son</string> + <string name="pref_title_notification_alert_vibrate">Notifier avec une vibration</string> + <string name="pref_title_notification_alert_light">Notifier avec une lumière</string> + <string name="pref_title_notification_filters">Me notifier quand</string> + <string name="pref_title_notification_filter_mentions">on me mentionne</string> + <string name="pref_title_notification_filter_follows">on vient de me suivre</string> + <string name="pref_title_notification_filter_reblogs">mes messages sont partagés</string> + <string name="pref_title_notification_filter_favourites">mes messages sont mis en favoris</string> + <string name="pref_title_appearance_settings">Apparence</string> + <string name="pref_title_app_theme">Thème de l’application</string> + <string name="pref_title_timelines">Fils chronologiques</string> + <string name="pref_title_timeline_filters">Filtres</string> + <string name="app_them_dark">Sombre</string> + <string name="app_theme_light">Clair</string> + <string name="app_theme_black">Noir</string> + <string name="app_theme_auto">Basé sur le coucher du soleil</string> + <string name="app_theme_system">Utiliser le thème système</string> + <string name="pref_title_browser_settings">Navigateur</string> + <string name="pref_title_custom_tabs">Utiliser le navigateur intégré</string> + <string name="pref_title_language">Langue</string> + <string name="pref_title_post_filter">Filtrage des fils</string> + <string name="pref_title_post_tabs">Fil principal</string> + <string name="pref_title_show_boosts">Afficher les partages</string> + <string name="pref_title_show_replies">Afficher les réponses</string> + <string name="pref_title_show_media_preview">Montrer les miniatures des médias</string> + <string name="pref_title_proxy_settings">Proxy</string> + <string name="pref_title_http_proxy_settings">Proxy HTTP</string> + <string name="pref_title_http_proxy_enable">Activer le proxy HTTP</string> + <string name="pref_title_http_proxy_server">Adresse du proxy HTTP</string> + <string name="pref_title_http_proxy_port">Port du proxy HTTP</string> + <string name="pref_default_post_privacy">Confidentialité par défaut</string> + <string name="pref_default_media_sensitivity">Toujours marquer les médias comme sensibles</string> + <string name="pref_publishing">Publication (synchronisée avec le serveur)</string> + <string name="pref_failed_to_sync">Échec de synchronisation des paramètres</string> + <string name="post_privacy_public">Public</string> + <string name="post_privacy_unlisted">Non listé</string> + <string name="post_privacy_followers_only">Abonné·e·s uniquement</string> + <string name="pref_post_text_size">Taille du texte des messages</string> + <string name="post_text_size_smallest">Plus petit</string> + <string name="post_text_size_small">Petit</string> + <string name="post_text_size_medium">Moyen</string> + <string name="post_text_size_large">Grand</string> + <string name="post_text_size_largest">Plus grand</string> + <string name="notification_mention_name">Nouvelles mentions</string> + <string name="notification_mention_descriptions">Notifications pour les nouvelles mentions</string> + <string name="notification_follow_name">Nouveaux abonnés</string> + <string name="notification_follow_description">Notifications pour les nouveaux abonnés</string> + <string name="notification_boost_name">Partages</string> + <string name="notification_boost_description">Notifications quand vos messages sont partagés</string> + <string name="notification_favourite_name">Favoris</string> + <string name="notification_favourite_description">Notifications quand vos messages sont mis en favoris</string> + <string name="notification_mention_format">%1$s vous a mentionné</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s et %4$d autres</string> + <string name="notification_summary_medium">%1$s, %2$s et %3$s</string> + <string name="notification_summary_small">%1$s et %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d nouvelle interaction</item> + <item quantity="many">%1$d nouvelles interactions</item> + <item quantity="other">%1$d nouvelles interactions</item> + </plurals> + <string name="description_account_locked">Compte verrouillé</string> + <string name="about_title_activity">À propos</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tusky est une application libre et open source. + Elle est publiée sous licence publique générale GNU version 3. + Vous pouvez consulter la licence ici : https://www.gnu.org/licenses/gpl-3.0.fr.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site"> Site du projet :\n + https://tusky.app + </string> + <string name="about_bug_feature_request_site"> Rapports d’anomalies & demandes de fonctionnalités :\n + https://github.com/tuskyapp/Tusky/issues + </string> + <string name="about_tusky_account">Profil de Tusky</string> + <string name="post_share_content">Partager le contenu du message</string> + <string name="post_share_link">Partager le lien du message</string> + <string name="post_media_images">Images</string> + <string name="post_media_video">Vidéo</string> + <string name="state_follow_requested">Demande d’abonnement effectuée</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">dans %1$da</string> + <string name="abbreviated_in_days">dans %1$dj</string> + <string name="abbreviated_in_hours">dans %1$dh</string> + <string name="abbreviated_in_minutes">dans %1$dm</string> + <string name="abbreviated_in_seconds">dans %1$ds</string> + <string name="abbreviated_years_ago">%1$d a</string> + <string name="abbreviated_days_ago">%1$dj</string> + <string name="abbreviated_hours_ago">%1$d h</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="follows_you">Vous suit</string> + <string name="pref_title_alway_show_sensitive_media">Toujours afficher les contenus sensibles</string> + <string name="title_media">Média</string> + <string name="replying_to">Réponse à @%1$s</string> + <string name="load_more_placeholder_text">en charger plus</string> + <string name="pref_title_public_filter_keywords">Fils publics</string> + <string name="pref_title_thread_filter_keywords">Conversations</string> + <string name="filter_addition_title">Ajouter un filtre</string> + <string name="filter_edit_title">Modifier un filtre</string> + <string name="filter_dialog_remove_button">Supprimer</string> + <string name="filter_dialog_update_button">Mettre à jour</string> + <string name="filter_add_description">Phrase à filtrer</string> + <string name="add_account_name">Ajouter un compte</string> + <string name="add_account_description">Ajouter un nouveau compte Mastodon</string> + <string name="action_lists">Listes</string> + <string name="title_lists">Listes</string> + <string name="error_create_list">Impossible de créer la liste</string> + <string name="error_rename_list">Impossible de renommer la liste</string> + <string name="error_delete_list">Impossible de supprimer la liste</string> + <string name="action_create_list">Créer une liste</string> + <string name="action_rename_list">Renommer la liste</string> + <string name="action_delete_list">Supprimer la liste</string> + <string name="hint_search_people_list">Chercher des personnes que vous suivez</string> + <string name="action_add_to_list">Ajouter un compte à la liste</string> + <string name="action_remove_from_list">Supprimer un compte de la liste</string> + <string name="compose_active_account_description">Publier en tant que %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Décrire pour les malvoyants (%1$d caractère maximum)</item> + <item quantity="many"/> + <item quantity="other">Décrire pour les malvoyants (%1$d caractères maximum)</item> + </plurals> + <string name="action_set_caption">Mettre une légende</string> + <string name="action_remove">Supprimer le média</string> + <string name="lock_account_label">Verrouiller le compte</string> + <string name="lock_account_label_description">Vous devez approuver manuellement les abonnements</string> + <string name="compose_save_draft">Enregistrer comme brouillon ?</string> + <string name="send_post_notification_title">Envoi du message…</string> + <string name="send_post_notification_error_title">Erreur lors de l’envoi du message</string> + <string name="send_post_notification_channel_name">Envoi des messages</string> + <string name="send_post_notification_cancel_title">Envoi annulé</string> + <string name="send_post_notification_saved_content">Une copie du message a été sauvegardée dans vos brouillons</string> + <string name="action_compose_shortcut">Écrire</string> + <string name="error_no_custom_emojis">Votre instance %1$s n’a pas d’émojis personnalisés</string> + <string name="emoji_style">Style d’émojis</string> + <string name="system_default">Par défaut du système</string> + <string name="download_fonts">Vous devez commencer par télécharger ces jeux d’émojis</string> + <string name="performing_lookup_title">Recherche en cours…</string> + <string name="expand_collapse_all_posts">Déplier/replier tout les messages</string> + <string name="action_open_post">Ouvrir le message</string> + <string name="restart_required">Un redémarrage de l’application est nécessaire</string> + <string name="restart_emoji">Vous devrez redémarrer Tusky pour appliquer ces modifications</string> + <string name="later">Plus tard</string> + <string name="restart">Redémarrer</string> + <string name="caption_systememoji">Votre jeu d’émojis par défaut</string> + <string name="caption_blobmoji">Un jeu d’émojis basé sur les émojis « blob »</string> + <string name="caption_twemoji">Le jeu d’émojis standard de Mastodon</string> + <string name="download_failed">Échec du téléchargement</string> + <string name="profile_badge_bot_text">Robot</string> + <string name="account_moved_description">%1$s a déménagé vers :</string> + <string name="reblog_private">Partager à l’audience originale</string> + <string name="unreblog_private">Annuler le partage</string> + <string name="license_description">Tusky contient du code et des ressources issus des projets open source suivants :</string> + <string name="license_apache_2">Sous licence Apache (copie ci-dessous)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Métadonnées du profil</string> + <string name="profile_metadata_add">Ajouter des données</string> + <string name="profile_metadata_label_label">Étiquette</string> + <string name="profile_metadata_content_label">Contenu</string> + <string name="pref_title_absolute_time">Utiliser le format de temps absolu</string> + <string name="label_remote_account">Les informations ci-dessous peuvent ne pas refléter le profil complet de l’utilisateur. Appuyez pour ouvrir le profil complet dans le navigateur.</string> + <string name="unpin_action">Détacher</string> + <string name="pin_action">Épingler</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Favori</item> + <item quantity="many"><b>%1$s</b> de favoris</item> + <item quantity="other"><b>%1$s</b> Favoris</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> Partage</item> + <item quantity="many"><b>%1$s</b> de partages</item> + <item quantity="other"><b>%1$s</b> Partages</item> + </plurals> + <string name="title_reblogged_by">Partagé par</string> + <string name="title_favourited_by">Mis en favoris par</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s et %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s et %3$d autres</string> + <string name="description_post_media">Média : %1$s</string> + <string name="description_post_cw"> Avertissement : %1$s + </string> + <string name="description_post_media_no_description_placeholder">Aucune description</string> + <string name="description_post_reblogged">Partagé</string> + <string name="description_post_favourited">Mis en favoris</string> + <string name="description_visibility_public">Public</string> + <string name="description_visibility_unlisted">Non listé</string> + <string name="description_visibility_private"> Abonné·e·s + </string> + <string name="description_visibility_direct">Direct</string> + <string name="hint_list_name">Nom de la liste</string> + <string name="edit_hashtag_hint">Hashtag sans #</string> + <string name="notifications_clear">Nettoyer</string> + <string name="notifications_apply_filter">Filtrer</string> + <string name="filter_apply">Appliquer</string> + <string name="compose_shortcut_long_label">Écrire un message</string> + <string name="compose_shortcut_short_label">Écrire</string> + <string name="pref_title_bot_overlay">Afficher l\'indicateur de robots</string> + <string name="notification_clear_text">Désirez-vous nettoyer toutes vos notifications de façon permanente \?</string> + <string name="action_delete_and_redraft">Effacer et ré-écrire</string> + <string name="dialog_redraft_post_warning">Effacer et ré-écrire ce message \?</string> + <string name="poll_info_time_absolute">Termina à %1$s</string> + <string name="poll_info_closed">Terminé</string> + <string name="poll_vote">Voter</string> + <string name="notification_poll_name">Sondages</string> + <string name="pref_title_notification_filter_poll">les sondages se terminent</string> + <string name="notification_poll_description">Notifications pour les sondages terminés</string> + <string name="poll_ended_created">Un sondage que vous avez créé est terminé</string> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d jour restant</item> + <item quantity="many">%1$d jours restants</item> + <item quantity="other">%1$d jours restants</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d heure restante</item> + <item quantity="many">%1$d heures restantes</item> + <item quantity="other">%1$d heures restantes</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d minute restante</item> + <item quantity="many">%1$d minutes restantes</item> + <item quantity="other">%1$d minutes restantes</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d seconde restante</item> + <item quantity="many">%1$d secondes restantes</item> + <item quantity="other">%1$d secondes restantes</item> + </plurals> + <string name="pref_title_animate_gif_avatars">Activer l’animation des avatars</string> + <string name="compose_preview_image_description">Actions pour l’image %1$s</string> + <string name="poll_info_format"> <!-- 15 votes • 1 heure restante --> %1$s • %2$s</string> + <string name="poll_ended_voted">Un sondage auquel vous avez participé vient de se terminer</string> + <string name="description_poll">Sondage avec des choix : %1$s, %2$s, %3$s, %4$s ; %5$s</string> + <string name="title_domain_mutes">Domaines cachés</string> + <string name="action_view_domain_mutes">Domaines cachés</string> + <string name="action_mute_domain">Masquer %1$s</string> + <string name="confirmation_domain_unmuted">%1$s n’est plus masqué·e</string> + <string name="mute_domain_warning_dialog_ok">Masquer le domaine entier</string> + <string name="caption_notoemoji">L’ensemble d’émojis actuel de Google</string> + <string name="button_continue">Continuer</string> + <string name="button_back">Retour</string> + <string name="report_sent_success">\@%1$s signalé avec succès</string> + <string name="hint_additional_info">Commentaires additionnels</string> + <string name="report_remote_instance">Transférer à %1$s</string> + <string name="failed_report">Échec du signalement</string> + <string name="failed_fetch_posts">Échec de récupération des messages</string> + <string name="report_description_1">Le rapport sera envoyé aux modérateur·rice·s de votre instance. Vous pouvez expliquer pourquoi vous signalez le compte ci-dessous :</string> + <string name="mute_domain_warning">Êtes-vous sûr⋅e de vouloir bloquer %1$s en entier \? Vous ne verrez plus de contenu provenant de ce domaine, ni dans les fils publics, ni dans vos notifications. Vos abonné·e·s utilisant ce domaine seront retiré·e·s.</string> + <string name="button_done">Terminé</string> + <string name="report_description_remote_instance">Le compte provient d’un autre serveur. Envoyez également une copie anonyme du rapport \?</string> + <string name="filter_dialog_whole_word">Mot entier</string> + <string name="filter_dialog_whole_word_description">Un mot-clé ou une phrase alphanumérique sera appliqué·e seulement s’il ou elle correspond au mot entier</string> + <string name="title_accounts">Comptes</string> + <string name="failed_search">Échec de la recherche</string> + <string name="pref_title_alway_open_spoiler">Toujours ouvrir les messages ayant un contenu sensible</string> + <string name="action_add_poll">Ajouter un sondage</string> + <string name="create_poll_title">Sondage</string> + <string name="duration_5_min">5 minutes</string> + <string name="duration_30_min">30 minutes</string> + <string name="duration_1_hour">1 heure</string> + <string name="duration_6_hours">6 heures</string> + <string name="duration_1_day">1 jour</string> + <string name="duration_3_days">3 jours</string> + <string name="duration_7_days">7 jours</string> + <string name="add_poll_choice">Ajouter un choix</string> + <string name="poll_allow_multiple_choices">Choix multiples</string> + <string name="poll_new_choice_hint">Choix %1$d</string> + <string name="edit_poll">Modifier</string> + <string name="title_scheduled_posts">Pouets planifiés</string> + <string name="action_edit">Modifier</string> + <string name="action_access_scheduled_posts">Messages programmés</string> + <string name="action_schedule_post">Planifier le message</string> + <string name="action_reset_schedule">Réinitialiser</string> + <string name="post_lookup_error_format">Erreur lors de la recherche du post %1$s</string> + <string name="about_powered_by_tusky">Propulsé par Tusky</string> + <string name="title_bookmarks">Signets</string> + <string name="action_bookmark">Marquer comme signet</string> + <string name="action_view_bookmarks">Signets</string> + <string name="description_post_bookmarked">Ajouté aux signets</string> + <string name="select_list_title">Sélectionner la liste</string> + <string name="list">Liste</string> + <string name="no_drafts">Vous n’avez aucun brouillon.</string> + <string name="no_scheduled_posts">Vous n’avez aucun message planifié.</string> + <string name="warning_scheduling_interval">L’intervalle minimum de planification sur Mastodon est de 5 minutes.</string> + <string name="notification_follow_request_name">Demandes d\'abonnement</string> + <string name="dialog_block_warning">Bloquer @%1$s \?</string> + <string name="pref_title_confirm_reblogs">Demander confirmation avant de partager</string> + <string name="pref_title_show_cards_in_timelines">Afficher les aperçus des liens dans les fils</string> + <string name="notification_follow_request_format">%1$s vous a envoyé une demande d’abonnement</string> + <string name="notification_follow_request_description">Notifications à propos des demandes d’abonnement</string> + <string name="pref_title_notification_filter_follow_requests">on demande à me suivre</string> + <string name="dialog_mute_warning">Mettre en sourdine @%1$s \?</string> + <string name="action_unmute_conversation">Enlever la sourdine à la conversation</string> + <string name="action_mute_conversation">Masquer la conversation</string> + <string name="pref_title_enable_swipe_for_tabs">Activer le glissement pour changer d’onglet</string> + <string name="hashtags">Hashtags</string> + <string name="add_hashtag_title">Ajouter un hashtag</string> + <string name="pref_title_gradient_for_media">Afficher des dégradés en couleur pour les médias cachés</string> + <string name="pref_main_nav_position_option_bottom">Bas</string> + <string name="pref_main_nav_position_option_top">Haut</string> + <string name="pref_main_nav_position">Position de navigation principale</string> + <string name="action_unmute_domain">Ne plus masquer %1$s</string> + <string name="dialog_mute_hide_notifications">Cacher les notifications</string> + <string name="action_unmute_desc">Ne plus masquer %1$s</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s personne</item> + <item quantity="many">%1$s personnes</item> + <item quantity="other">%1$s personnes</item> + </plurals> + <string name="pref_title_hide_top_toolbar">Cacher le titre de la barre d’outils supérieure</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s voix</item> + <item quantity="many">%1$s voix</item> + <item quantity="other">%1$s voix</item> + </plurals> + <string name="account_note_saved">Sauvegardé !</string> + <string name="account_note_hint">Votre note privée sur ce compte</string> + <string name="no_announcements">Il n’y a pas d’annonce.</string> + <string name="title_announcements">Annonces</string> + <string name="wellbeing_mode_notice">Certaines informations susceptibles d’affecter votre bien-être mental seront cachées. Il s’agit : +\n +\n - des notifications de favoris, de partage et de suivi +\n - du nombre des favoris/partages des messages +\n - des statistiques sur les profils +\n +\n Les notifications « push » ne seront pas affectées, mais vous pouvez revoir vos préférences de notification manuellement.</string> + <string name="pref_title_notification_filter_subscriptions">un compte auquel je suis abonné·e a publié un nouveau message</string> + <string name="notification_subscription_format">%1$s vient de publier</string> + <string name="review_notifications">Examiner les notifications</string> + <string name="notification_subscription_name">Nouveaux messages</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Vous ne pouvez pas téléverser plus d’ %1$d pièce jointe.</item> + <item quantity="many">Vous ne pouvez pas téléverser plus de %1$d pièces jointes.</item> + <item quantity="other">Vous ne pouvez pas téléverser plus de %1$d pièces jointes.</item> + </plurals> + <string name="pref_title_wellbeing_mode">Bien-être</string> + <string name="notification_subscription_description">Notifications quand quelqu’un que vous suivez publie un nouveau message</string> + <string name="limit_notifications">Limiter les notifications du fil</string> + <string name="wellbeing_hide_stats_profile">Cacher les statistiques quantitatives sur les profils</string> + <string name="wellbeing_hide_stats_posts">Cacher les statistiques quantitatives sur les messages</string> + <string name="action_unbookmark">Supprimer le signet</string> + <string name="post_media_attachments">Pièces jointes</string> + <string name="dialog_delete_list_warning">Voulez-vous vraiment supprimer la liste %1$s \?</string> + <string name="action_subscribe_account">S’abonner</string> + <string name="dialog_delete_conversation_warning">Supprimer cette conversation \?</string> + <string name="pref_title_animate_custom_emojis">Animer les émojis personnalisés</string> + <string name="draft_deleted">Brouillon supprimé</string> + <string name="label_duration">Durée</string> + <string name="duration_indefinite">Indéfinie</string> + <string name="action_unsubscribe_account">Se désabonner</string> + <string name="action_delete_conversation">Supprimer la conversation</string> + <string name="post_media_audio">Audio</string> + <string name="pref_title_confirm_favourites">Demander confirmation avant de mettre en favoris</string> + <string name="drafts_post_reply_removed">Le message auquel répondait ce brouillon a été supprimé</string> + <string name="drafts_post_failed_to_send">Échec d’envoi du message !</string> + <string name="follow_requests_info">Bien que votre compte ne soit pas verrouillé, l’équipe de %1$s a pensé que vous voudriez valider manuellement les demandes de d’abonnement provenant de ces comptes.</string> + <string name="drafts_failed_loading_reply">Échec du chargement des informations de réponse</string> + <string name="duration_30_days">30 jours</string> + <string name="duration_60_days">60 jours</string> + <string name="duration_90_days">90 jours</string> + <string name="duration_365_days">365 jours</string> + <string name="duration_14_days">14 jours</string> + <string name="duration_180_days">180 jours</string> + <string name="tusky_compose_post_quicksetting_label">Rédiger un message</string> + <string name="notification_sign_up_format">%1$s a créé un compte</string> + <string name="notification_sign_up_name">Nouveaux comptes</string> + <string name="notification_sign_up_description">Notifications quand quelqu’un crée un nouveau compte</string> + <string name="pref_title_notification_filter_sign_ups">un nouveau compte a été créé</string> + <string name="notification_update_format">%1$s a modifié son message</string> + <string name="pref_title_notification_filter_updates">un message avec lequel j’ai interagi est modifié</string> + <string name="notification_update_name">Messages modifiés</string> + <string name="notification_update_description">Notifications quand un message avec lequel vous avez interagi est modifié</string> + <string name="title_login">Se connecter</string> + <string name="account_date_joined">Ici depuis %1$s</string> + <string name="action_details">Détails</string> + <string name="saving_draft">Sauvegarde du brouillon …</string> + <string name="status_count_one_plus">>1</string> + <string name="dialog_push_notification_migration_other_accounts">Tusky peut maintenant recevoir les notifications instantanées de ce compte. Cependant, d\'autres de vos comptes n\'ont pas encore accès aux notifications instantanées. Basculez sur chacun de vos comptes et reconnectez les afin de recevoir les notifications avec UnifiedPush.</string> + <string name="error_could_not_load_login_page">La page de connexion ne peut être chargée.</string> + <string name="action_edit_image">Retoucher l’image</string> + <string name="action_dismiss">Fermer</string> + <string name="title_migration_relogin">Se reconnecter pour recevoir les notifications instantanées</string> + <string name="dialog_push_notification_migration">Afin de recevoir les notifications via UnifiedPush, Tusky doit demander à votre serveur Mastodon la permission de s’inscrire aux notifications. Ceci nécessite une reconnexion de vos comptes afin de changer les droits OAuth accordés a Tusky. En utilisant l’option de reconnexion ici ou dans les préférences de compte, vos brouillons et le cache seront préservés.</string> + <string name="tips_push_notification_migration">Reconnectez tous vos comptes pour activer les notifications instantanées.</string> + <string name="error_image_edit_failed">L\'image n’a pas pu être retouchée.</string> + <string name="delete_scheduled_post_warning">Supprimer ce message planifié \?</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="pref_show_self_username_always">Toujours</string> + <string name="pref_show_self_username_never">Jamais</string> + <string name="description_post_language">Langue du message</string> + <string name="duration_no_change">(Aucune modification)</string> + <string name="action_add_reaction">ajouter une réaction</string> + <string name="instance_rule_title">%1$s règles</string> + <string name="a11y_label_loading_thread">Chargement du fil</string> + <string name="error_following_hashtags_unsupported">Cette instance ne prend pas en charge le suivi des hashtags.</string> + <string name="error_muting_hashtag_format">Une erreur est survenue en masquant #%1$s</string> + <string name="error_unmuting_hashtag_format">Une erreur est survenue en arrêtant de masquer #%1$s</string> + <string name="error_following_hashtag_format">Une erreur est survenue en suivant #%1$s</string> + <string name="error_unfollowing_hashtag_format">Une erreur est survenue en arrêtant de suivre #%1$s</string> + <string name="action_add_or_remove_from_list">Ajouter à ou retirer de la liste</string> + <string name="failed_to_remove_from_list">Le compte n\'a pas pu être retiré de la liste</string> + <string name="failed_to_add_to_list">Le compte n\'a pas pu être ajouté à la liste</string> + <string name="dialog_follow_hashtag_hint">#hashtag</string> + <string name="dialog_follow_hashtag_title">Suivre un hashtag</string> + <string name="hint_media_description_missing">Le contenu média devrait être décrit.</string> + <string name="pref_summary_http_proxy_invalid"><invalide></string> + <string name="pref_summary_http_proxy_disabled">Désactivé</string> + <string name="pref_summary_http_proxy_missing"><non spécifié></string> + <string name="title_edits">Modifications</string> + <string name="pref_default_post_language">Langue de publication par défaut</string> + <string name="error_multimedia_size_limit">Les fichiers vidéo et audio ne peuvent pas dépasser %1$s Mo.</string> + <string name="pref_title_http_proxy_port_message">Le port doit être entre %1$d et %2$d</string> + <string name="action_set_focus">Définir le point focal</string> + <string name="post_media_image">Image</string> + <string name="pref_show_self_username_disambiguate">Quand plusieurs comptes sont connectés</string> + <string name="action_post_failed">Le téléversement a échoué</string> + <string name="action_post_failed_show_drafts">Afficher les brouillons</string> + <string name="action_post_failed_do_nothing">Ignorer</string> + <string name="post_media_alt">ALT</string> + <string name="notification_report_format">Nouveau signalement sur %1$s</string> + <string name="notification_header_report_format">%1$s a signalé %2$s</string> + <string name="notification_summary_report_format">%1$s · %2$d posts joints</string> + <string name="notification_report_name">Signalements</string> + <string name="set_focus_description">Appuyez ou faites glissez le cercle pour déplacer le point focal, qui sera toujours visible sur les vignettes.</string> + <string name="notification_unknown_name">Inconnu</string> + <string name="action_browser_login">Se connecter avec le navigateur</string> + <string name="send_account_link_to">Partager le lien du compte avec…</string> + <string name="account_username_copied">Nom de compte copié</string> + <string name="action_discard">Abandonner les modifications</string> + <string name="action_continue_edit">Continuer à modifier</string> + <string name="confirmation_hashtag_unfollowed">#%1$s n\'est plus suivi</string> + <string name="post_edited">Modifié le %1$s</string> + <string name="notification_report_description">Notifications à propos des signalements</string> + <string name="action_share_account_link">Partager le lien du compte</string> + <string name="action_share_account_username">Partager le nom du compte</string> + <string name="title_followed_hashtags">Hashtags suivis</string> + <string name="pref_title_notification_filter_reports">il y a un nouveau signalement</string> + <string name="pref_title_account_filter_keywords">Profils</string> + <string name="status_filtered_show_anyway">Montrer quand même</string> + <string name="status_filter_placeholder_label_format">Caché : %1$s</string> + <string name="title_public_trending_hashtags">Hashtags tendance</string> + <string name="send_account_username_to">Partager le nom du compte avec…</string> + <string name="status_created_at_now">maintenant</string> + <string name="error_loading_account_details">Les détails du compte n\'ont pas pu être chargés</string> + <string name="instance_rule_info">En vous connectant vous acceptez de vous tenir aux règles de %1$s.</string> + <string name="pref_title_reading_order">Ordre de lecture</string> + <string name="pref_reading_order_oldest_first">Les plus vieux en premier</string> + <string name="pref_reading_order_newest_first">Les plus récents en premier</string> + <string name="action_unfollow_hashtag_format">Arrêter de suivre #%1$s \?</string> + <string name="failed_to_pin">Échec de l\'épinglage</string> + <string name="status_edit_info">Modification : %1$s</string> + <string name="status_created_info">Création : %1$s</string> + <string name="compose_save_draft_loses_media">Enregistrer comme brouillon \? (Les pièces jointes seront à nouveau téléchargées lorsque le brouillon sera réouvert.)</string> + <string name="no_lists">Vous n\'avez aucune liste.</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="report_category_spam">Spam</string> + <string name="report_category_other">Autre</string> + <string name="total_accounts">comptes</string> + <string name="action_post_failed_detail">Le statut n\'a pas pu être publié, il a été sauvegardé dans les brouillons. +\n +\nLe serveur n\'est pas joignable, ou celui-ci a refusé la publication.</string> + <string name="action_post_failed_detail_plural">Les status n\'ont pas pu être publiés, ils ont été sauvegardés dans les brouillons. +\n +\nLe serveur n\'est pas joignable, ou celui-ci a refusé la publication.</string> + <string name="action_refresh">Actualiser</string> + <string name="failed_to_unpin">Échec du détachement</string> + <string name="pref_title_show_self_username">Montrer le nom d\'utilisateur dans la barre d\'outils</string> + <string name="socket_timeout_exception">La connexion à votre serveur a pris trop longtemps</string> + <string name="ui_error_bookmark">Échec de l\'ajout aux signets : %1$s</string> + <string name="ui_error_vote">Échec du vote : %1$s</string> + <string name="ui_error_reblog">Échec lors du partage de la publication : %1$s</string> + <string name="ui_error_accept_follow_request">N\'a pas pu accepter la demande d\'abonnement : %1$s</string> + <string name="ui_error_reject_follow_request">N\'a pas pu rejeter la demande d\'abonnement : %1$s</string> + <string name="ui_success_accepted_follow_request">Demande d\'abonnement acceptée</string> + <string name="ui_success_rejected_follow_request">Demande d\'abonnement rejetée</string> + <string name="description_login">Fonctionne dans la plupart des cas. Aucune autre application n\'aura accès à vos données.</string> + <string name="description_browser_login">Peut permettre des méthodes d\'authentification supplémentaires, mais un navigateur compatible est nécessaire.</string> + <string name="mute_notifications_switch">Masquer les notifications</string> + <string name="compose_unsaved_changes">Il y a des modifications non enregistrées.</string> + <string name="description_post_edited">Modifié</string> + <string name="select_list_manage">Gérer les listes</string> + <string name="report_category_violation">Règle enfreinte</string> + <string name="error_status_source_load">Le texte d\'origine du statut n\'a pas pu être chargé.</string> + <string name="ui_error_unknown">raison inconnue</string> + <string name="ui_error_clear_notifications">Échec du nettoyage des notifications : %1$s</string> + <string name="ui_error_favourite">Échec de la mise en favori : %1$s</string> + <string name="label_filter_title">Nom</string> + <string name="hint_filter_title">Mon filtre</string> + <string name="pref_title_show_stat_inline">Montrer les statistiques des statuts dans les fils</string> + <string name="accessibility_talking_about_tag">%1$d personnes parlent du hashtag %2$s</string> + <string name="total_usage">utilisations</string> + <string name="help_empty_home">Ceci est votre <b>fil d\'accueil</b>. Il affiche les publications récentes des comptes que vous suivez. +\n +\nPour trouver des comptes vous pouvez soit les découvrir dans l\'un des autres fils, par exemple le fil local de votre instance [iconics gmd_group] ; soit les chercher par leur nom [iconics gmd_search], par exemple cherchez « Tusky » pour trouver notre compte Mastodon.</string> + <string name="action_add">Ajouter</string> + <string name="label_filter_keywords">Mots-clés ou phrases à filtrer</string> + <string name="filter_description_warn">Cacher derrière un avertissement</string> + <string name="filter_description_hide">Cacher complètement</string> + <string name="label_filter_action">Effet du filtre</string> + <string name="filter_action_warn">Avertissement</string> + <string name="filter_action_hide">Cacher</string> + <string name="label_filter_context">Contextes filtrés</string> + <string name="filter_keyword_display_format">%1$s (mot entier)</string> + <string name="filter_keyword_addition_title">Ajouter un mot-clé</string> + <string name="filter_edit_keyword_title">Modifier mot-clé</string> + <string name="filter_description_format">%1$s : %2$s</string> + <string name="pref_ui_text_size">Taille de police de l\'interface</string> + <string name="pref_title_show_self_boosts">Afficher les auto-partages</string> + <string name="pref_title_show_self_boosts_description">Quelqu\'un partageant son propre message</string> + <string name="label_image">Image</string> + <string name="dialog_delete_filter_positive_action">Supprimer</string> + <string name="notification_prune_cache">Maintenance du cache …</string> + <string name="about_device_info">%1$s %2$s +\nVersion Android : %3$s +\nVersion SDK : %4$d</string> + <string name="about_device_info_title">Votre appareil</string> + <string name="about_account_info_title">Votre compte</string> + <string name="about_account_info">\@%1$s@%2$s +\nVersion : %3$s</string> + <string name="about_copy">Copier les informations de la version et de l’appareil</string> + <string name="title_public_trending_statuses">Publications en tendance</string> + <string name="list_reply_policy_list">Membres de la liste</string> + <string name="list_reply_policy_label">Afficher les réponses à</string> + <string name="about_copied">Les informations de la version et de l’appareil ont été copiés</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-fy/strings.xml b/app/src/main/res/values-fy/strings.xml new file mode 100644 index 0000000..bff6980 --- /dev/null +++ b/app/src/main/res/values-fy/strings.xml @@ -0,0 +1,259 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_invalid_domain">Ûnjildich domein ynfierd</string> + <string name="error_empty">Dit mei net leech wêze.</string> + <string name="system_default">Systeem standert</string> + <string name="emoji_style">Emoji styl</string> + <string name="action_compose_shortcut">Gearstelle</string> + <string name="send_post_notification_cancel_title">Ferstjoeren ôfbrutsen</string> + <string name="send_post_notification_channel_name">Toots oan it ferstjoeren</string> + <string name="send_post_notification_error_title">Flater by it ferstjoeren fan toot</string> + <string name="send_post_notification_title">Toot oan it ferstjoeren…</string> + <string name="compose_save_draft">Skets bewarje\?</string> + <string name="action_remove">Fuortsmite</string> + <string name="action_set_caption">Ûnderskrift pleatse</string> + <string name="action_add_to_list">Account oan de list tafoegje</string> + <string name="hint_search_people_list">Sykje om minsken dy\'t jo folgje</string> + <string name="action_delete_list">Smyt de list fuort</string> + <string name="action_rename_list">Neam de list om</string> + <string name="action_create_list">Meitsje in list oan</string> + <string name="error_delete_list">Koe list net fuortsmite</string> + <string name="error_rename_list">Koe list net omneame</string> + <string name="error_create_list">Koe list net oanmeitsje</string> + <string name="title_lists">Listen</string> + <string name="action_lists">Listen</string> + <string name="add_account_description">Nij Mastodon Account Tafoegje</string> + <string name="add_account_name">Account Tafoegje</string> + <string name="filter_dialog_update_button">Fernije</string> + <string name="filter_dialog_remove_button">Fuortsmite</string> + <string name="filter_edit_title">Filter oanpasse</string> + <string name="filter_addition_title">Filter tafoegje</string> + <string name="pref_title_thread_filter_keywords">Petearen</string> + <string name="load_more_placeholder_text">mear lade</string> + <string name="replying_to">Oan it reagearren op @%1$s</string> + <string name="title_media">Media</string> + <string name="pref_title_alway_show_sensitive_media">Altyd gefoeliche ynhâld sjen litte</string> + <string name="follows_you">Folget jo</string> + <string name="abbreviated_years_ago">%1$dy</string> + <string name="abbreviated_in_seconds">oer %1$ds</string> + <string name="abbreviated_in_minutes">oer %1$dm</string> + <string name="abbreviated_in_hours">oer %1$dh</string> + <string name="abbreviated_in_days">oer %1$dd</string> + <string name="post_media_attachments">Taheaksels</string> + <string name="post_media_audio">Lûd</string> + <string name="post_media_video">Fideo</string> + <string name="post_media_images">Ôfbyldingen</string> + <string name="post_share_link">Keppeling nei toot diele</string> + <string name="post_share_content">Ynhâld fan toot diele</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_title_activity">Oer</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d nije ynteraksje</item> + <item quantity="other">%1$d nije ynteraksjes</item> + </plurals> + <string name="notification_summary_small">%1$s en %2$s</string> + <string name="notification_summary_medium">%1$s, %2$s, en %3$s</string> + <string name="notification_subscription_name">Nije toots</string> + <string name="notification_favourite_name">Favoriten</string> + <string name="notification_follow_request_name">Folgfersyken</string> + <string name="notification_follow_name">Nije Folgers</string> + <string name="post_text_size_largest">Grutst</string> + <string name="post_text_size_large">Grut</string> + <string name="post_text_size_medium">Gewoan</string> + <string name="post_text_size_small">Lyts</string> + <string name="post_text_size_smallest">Lytst</string> + <string name="post_privacy_followers_only">Allinnich folgers</string> + <string name="post_privacy_public">Iepenbier</string> + <string name="pref_main_nav_position_option_bottom">Ûnder</string> + <string name="pref_main_nav_position_option_top">Boppe</string> + <string name="pref_failed_to_sync">Koe ynstellingen net syngronisearje</string> + <string name="pref_default_media_sensitivity">Media altyd as gefoelich oanmerke</string> + <string name="pref_title_http_proxy_port">HTTP proksje poarte</string> + <string name="pref_title_http_proxy_server">HTTP proksje tsjinner</string> + <string name="pref_title_http_proxy_enable">HTTP proksje ynskeakelje</string> + <string name="pref_title_http_proxy_settings">HTTP proksje</string> + <string name="pref_title_proxy_settings">Proksje</string> + <string name="pref_title_show_media_preview">Media foarfertoaningen delhelje</string> + <string name="pref_title_show_replies">Reaksjes sjen litte</string> + <string name="pref_title_post_tabs">Ljepblêden</string> + <string name="pref_title_language">Taal</string> + <string name="pref_title_browser_settings">Webblêder</string> + <string name="app_theme_system">Systeem Opmaak Brûke</string> + <string name="app_theme_auto">Automatysk as de sinne ûnder giet</string> + <string name="app_theme_black">Swart</string> + <string name="app_theme_light">Ljocht</string> + <string name="app_them_dark">Tsjuster</string> + <string name="pref_title_timeline_filters">Filters</string> + <string name="pref_title_app_theme">Applikaasje Tema</string> + <string name="pref_title_appearance_settings">Uterlik</string> + <string name="pref_title_notification_filter_subscriptions">Ien dy\'t ik folgje hat in nije toot pleatst</string> + <string name="pref_title_notification_filter_favourites">Myn berjochten bin as favoryt oanmurken</string> + <string name="pref_title_notification_filter_follow_requests">Folgfersyk</string> + <string name="pref_title_notification_filter_follows">Folgers</string> + <string name="pref_title_notification_filter_mentions">beneamd</string> + <string name="pref_title_notification_alert_light">Op\'e hichte steld wurde mei in ljochtsje</string> + <string name="pref_title_notification_alert_vibrate">Op\'e hichte steld wurde mei in trilling</string> + <string name="pref_title_notification_alert_sound">Op\'e hichte stelt wurde mei in lûdsje</string> + <string name="pref_title_notifications_enabled">Notifikaasjes</string> + <string name="pref_title_edit_notification_settings">Notifikaasjes</string> + <string name="dialog_mute_hide_notifications">Notifikaasjes ferbergje</string> + <string name="dialog_mute_warning">\@%1$s negearje\?</string> + <string name="dialog_block_warning">\@%1$s blokkearje\?</string> + <string name="mute_domain_warning_dialog_ok">Folsleine domein ferbergje</string> + <string name="dialog_delete_conversation_warning">Dit petear fuortsmite\?</string> + <string name="dialog_redraft_post_warning">Dizze toot fuortsmite en opnij opstelle\?</string> + <string name="dialog_delete_post_warning">Dizze toot fuortsmite\?</string> + <string name="dialog_unfollow_warning">Dit account net mear folgje\?</string> + <string name="dialog_message_cancel_follow_request">Folgfersyk ynlûke\?</string> + <string name="dialog_download_image">Delhelje</string> + <string name="dialog_message_uploading_media">Oan it uploaden…</string> + <string name="dialog_title_finishing_media_upload">It Uploaden fan Media oan it Ôfrûnjen</string> + <string name="login_connection">Oan it ferbinen…</string> + <string name="label_quick_reply">Reagearre…</string> + <string name="search_no_results">Gjin resultaten</string> + <string name="hint_search">Sykje…</string> + <string name="hint_content_warning">Ynhâld warskôging</string> + <string name="hint_compose">Wat bard der\?</string> + <string name="confirmation_domain_unmuted">%1$s net mear ferburgen</string> + <string name="confirmation_unmuted">Brûker net mear negearre</string> + <string name="confirmation_unblocked">Brûker net mear blokkearre</string> + <string name="confirmation_reported">Ferstjoerd!</string> + <string name="send_media_to">Media ferstjoere nei…</string> + <string name="send_post_content_to">Toot ferstjoere nei…</string> + <string name="send_post_link_to">Toot URL ferstjoere nei…</string> + <string name="downloading_media">Media oan it delheljen</string> + <string name="download_media">Media delhelje</string> + <string name="action_share_as">Diele as…</string> + <string name="action_open_as">Iepenje as %1$s</string> + <string name="action_copy_link">Keppeling kopiearje</string> + <string name="download_image">Oan it delheljen fan %1$s</string> + <string name="action_open_media_n">Media #%1$d iepenje</string> + <string name="title_links_dialog">Keppelingen</string> + <string name="action_open_faved_by">Favoriten besjen</string> + <string name="action_links">Keppelingen</string> + <string name="action_add_tab">Ljepblêd Tafoegje</string> + <string name="action_schedule_post">Toot ynplanne</string> + <string name="action_emoji_keyboard">Emoji toetseboerd</string> + <string name="action_content_warning">Ynhâld warskôging</string> + <string name="action_toggle_visibility">Toot sichtberheid</string> + <string name="action_access_scheduled_posts">Ynplanne toots</string> + <string name="action_access_drafts">Sketsen</string> + <string name="action_search">Sykje</string> + <string name="action_reject">Net akseptearje</string> + <string name="action_accept">Akseptearje</string> + <string name="action_undo">Ûngedien meitsje</string> + <string name="action_edit_own_profile">Oanpasse</string> + <string name="action_edit_profile">Profyl oanpasse</string> + <string name="action_save">Bewarje</string> + <string name="action_open_drawer">Laad iepenje</string> + <string name="action_hide_media">Media ferbergje</string> + <string name="action_mention">Beneame</string> + <string name="action_unmute_conversation">Petear net mear negearre</string> + <string name="action_mute_conversation">Petear negearre</string> + <string name="action_unmute_domain">%1$s net mear negearre</string> + <string name="action_mute_domain">%1$s negearre</string> + <string name="action_unmute_desc">%1$s net mear negearre</string> + <string name="action_unmute">Net mear negearre</string> + <string name="action_mute">Negearre</string> + <string name="action_share">Diele</string> + <string name="action_photo_take">Foto nimme</string> + <string name="action_add_poll">Fragelist tafoegje</string> + <string name="action_add_media">Media tafoegje</string> + <string name="action_open_in_web">Yn webblêder iepenje</string> + <string name="action_view_media">Media</string> + <string name="action_view_follow_requests">Folgfersyken</string> + <string name="action_view_domain_mutes">Ferburgen domeinen</string> + <string name="action_view_blocks">Blokkearre brûkers</string> + <string name="action_view_mutes">Negearre brûkers</string> + <string name="action_view_bookmarks">Blêdwiizers</string> + <string name="action_view_favourites">Favoriten</string> + <string name="action_view_account_preferences">Account Foarkarren</string> + <string name="action_view_preferences">Foarkarren</string> + <string name="action_view_profile">Profyl</string> + <string name="action_close">Slute</string> + <string name="action_retry">Opnij probearje</string> + <string name="action_send_public">TOOT!</string> + <string name="action_send">TOOT</string> + <string name="action_delete_and_redraft">Fuortsmite en opnij opstelle</string> + <string name="action_delete_conversation">Petear fuortsmite</string> + <string name="action_delete">Fuortsmite</string> + <string name="action_edit">Oanpasse</string> + <string name="action_report">Oanjaan</string> + <string name="action_unblock">Net mear blokkearje</string> + <string name="action_block">Blokkearje</string> + <string name="action_unfollow">Net mear folgje</string> + <string name="action_follow">Folgje</string> + <string name="action_logout">Útlogge</string> + <string name="action_login">Ynlogge mei Mastodon</string> + <string name="action_compose">Gearstelle</string> + <string name="action_more">Mear</string> + <string name="action_unfavourite">Net mear as favoryt oanmerke</string> + <string name="action_favourite">As favoryt oanmerke</string> + <string name="action_reply">Reagearje</string> + <string name="action_quick_reply">Flugge Reaksje</string> + <string name="report_comment_hint">Oanfoljende opmerkingen\?</string> + <string name="report_username_format">Jou @%1$s oan</string> + <string name="notification_subscription_format">%1$s hat krekt in berjocht pleatst</string> + <string name="notification_follow_request_format">%1$s fersiket jo te folgjen</string> + <string name="notification_follow_format">%1$s folget jo</string> + <string name="notification_favourite_format">%1$s hat jo toot as favoryt oanmurken</string> + <string name="footer_empty">Hjir is neat. Lûk nei ûnderen om te ferfarskjen!</string> + <string name="message_empty">Hjir is neat.</string> + <string name="post_content_show_less">Yntearre</string> + <string name="post_content_show_more">Ûttearre</string> + <string name="post_content_warning_show_less">Minder sjen litte</string> + <string name="post_content_warning_show_more">Mear sjen litte</string> + <string name="post_sensitive_media_directions">Klik om te besjen</string> + <string name="post_media_hidden_title">Media ferburgen</string> + <string name="post_sensitive_media_title">Gefoelige ynhâld</string> + <string name="post_username_format">\@%1$s</string> + <string name="title_licenses">Lisinsjes</string> + <string name="title_scheduled_posts">Ynplanne toots</string> + <string name="title_drafts">Sketsen</string> + <string name="title_edit_profile">Jo profyl oanpasse</string> + <string name="title_follow_requests">Folgfersyken</string> + <string name="title_domain_mutes">Ferburgen domeinen</string> + <string name="title_blocks">Blokkearre brûkers</string> + <string name="title_mutes">Negearre brûkers</string> + <string name="title_bookmarks">Blêdwiizers</string> + <string name="title_favourites">Favoriten</string> + <string name="title_followers">Folgers</string> + <string name="title_follows">Folget</string> + <string name="title_posts_pinned">Fêstset</string> + <string name="title_posts_with_replies">Mei reaksjes</string> + <string name="title_posts">Berjochten</string> + <string name="title_view_thread">Toot</string> + <string name="title_tab_preferences">Ljepblêden</string> + <string name="title_direct_messages">Direkte Berjochten</string> + <string name="title_public_federated">Federearre</string> + <string name="title_public_local">Lokaal</string> + <string name="title_notifications">Notifikaasjes</string> + <string name="title_home">Thús</string> + <string name="error_sender_account_gone">Flater by it ferstjoeren fan de toot.</string> + <string name="error_media_upload_sending">De upload is mislearre.</string> + <string name="error_media_upload_image_or_video">Ôfbyldingen en fideo\'s kinne net beide taheake wêze oan deselde status.</string> + <string name="error_media_download_permission">Tastimming om media op te slaan is nedich.</string> + <string name="error_media_upload_permission">Tastimming om media te lêzen is nedich.</string> + <string name="error_media_upload_opening">Die triem koe net iepene wurde.</string> + <string name="error_media_upload_type">Dat type triem kin net upload wurde.</string> + <string name="error_compose_character_limit">De status is te lang!</string> + <string name="error_retrieving_oauth_token">Koe gjin ynlogtoken krije.</string> + <string name="error_authorization_denied">Ferifikaasje ôfkard.</string> + <string name="error_authorization_unknown">Der die harren in net definiearre flater foar.</string> + <string name="error_no_web_browser_found">Koe gjin webblêder fine om te brûken.</string> + <string name="error_network">In netwurk flater die harren foar! Kontrolearje jo ferbining en probearje it noch ris!</string> + <string name="error_generic">Der die harren in flater foar.</string> + <string name="label_avatar">Profylôfbylding</string> + <string name="hint_note">Oer dy</string> + <string name="title_hashtags_dialog">Hashtags</string> + <string name="action_open_reblogged_by">Boosts sjen litte</string> + <string name="action_open_reblogger">Auteur fan boost iepenje</string> + <string name="action_hashtags">Hashtags</string> + <string name="action_reset_schedule">Nei standert ynstelling weromsette</string> + <string name="action_show_reblogs">Boosts sjen litte</string> + <string name="action_hide_reblogs">Boosts ferburgje</string> + <string name="action_unreblog">Boost fuorthelje</string> + <string name="action_reblog">Boost</string> + <string name="notification_reblog_format">%1$s hat dyn toot boost</string> + <string name="title_announcements">Oankundigingen</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-ga/strings.xml b/app/src/main/res/values-ga/strings.xml new file mode 100644 index 0000000..ebf65fe --- /dev/null +++ b/app/src/main/res/values-ga/strings.xml @@ -0,0 +1,500 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="post_content_show_more">Leathnaigh</string> + <string name="post_content_warning_show_less">Taispeáin Níos Lú</string> + <string name="post_content_warning_show_more">Taispeáin Níos Mó</string> + <string name="post_sensitive_media_directions">Cliceáil chun amharc</string> + <string name="post_media_hidden_title">Meáin i bhfolach</string> + <string name="post_sensitive_media_title">Ábhar íogair</string> + <string name="post_boosted_format">D\'athchraol %1$s</string> + <string name="title_licenses">Ceadúnais</string> + <string name="title_scheduled_posts">Postálacha sceidealta</string> + <string name="title_edit_profile">Cuir do phróifíl in eagar</string> + <string name="title_follow_requests">Lean Iarrataí</string> + <string name="title_domain_mutes">Fearainn i bhfolach</string> + <string name="title_blocks">Úsáideoirí blocáilte</string> + <string name="title_mutes">Úsáideoirí fuaim</string> + <string name="title_bookmarks">Leabharmharcanna</string> + <string name="title_followers">Leantóirí</string> + <string name="title_follows">Ag Leanúint</string> + <string name="title_posts_pinned">Greamaithe</string> + <string name="title_posts_with_replies">Le freagraí</string> + <string name="title_view_thread">Snáithe</string> + <string name="title_direct_messages">Teachtaireachtaí Díreacha</string> + <string name="title_public_federated">Cónaidhme</string> + <string name="title_public_local">Áitiúil</string> + <string name="title_notifications">Fógraí</string> + <string name="title_home">Baile</string> + <string name="error_sender_account_gone">Earráid agus an toot á sheoladh.</string> + <string name="error_media_upload_sending">Theip ar an uaslódáil.</string> + <string name="error_media_upload_image_or_video">Ní féidir íomhánna agus físeáin a cheangal leis an stádas céanna.</string> + <string name="error_media_download_permission">Teastaíonn cead chun na meáin a stóráil.</string> + <string name="error_media_upload_permission">Teastaíonn cead chun na meáin a léamh.</string> + <string name="error_media_upload_opening">Ní fhéadfaí an comhad sin a oscailt.</string> + <string name="pref_title_custom_tabs">Úsáid Chrome Custom Tabs</string> + <string name="pref_title_browser_settings">Brabhsálaí</string> + <string name="app_theme_system">Úsáid Dearadh Córais</string> + <string name="app_theme_auto">Uathoibríoch ag luí na gréine</string> + <string name="app_theme_black">Dubh</string> + <string name="app_theme_light">Éadrom</string> + <string name="app_them_dark">Dorcha</string> + <string name="pref_title_timeline_filters">Scagairí</string> + <string name="pref_title_timelines">Amlínte</string> + <string name="pref_title_app_theme">Téama an Aip</string> + <string name="pref_title_appearance_settings">Dealramh</string> + <string name="pref_title_notification_filter_poll">tá deireadh leis na pobalbhreitheanna</string> + <string name="pref_title_notification_filter_favourites">tá mo chuid postálacha tofa</string> + <string name="pref_title_notification_filter_reblogs">athchraoltar mó chuid postálacha</string> + <string name="pref_title_notification_filter_follow_requests">lean iarrtar</string> + <string name="pref_title_notification_filter_follows">lean</string> + <string name="pref_title_notification_filter_mentions">luaigh</string> + <string name="pref_title_notification_filters">Cuir in iúl dom cathain</string> + <string name="pref_title_notification_alert_light">Fógra le solas</string> + <string name="pref_title_notification_alert_vibrate">Cuir in iúl le tonnchrith</string> + <string name="pref_title_notification_alert_sound">Fógra le fuaim</string> + <string name="pref_title_notification_alerts">Foláirimh</string> + <string name="pref_title_notifications_enabled">Fógraí</string> + <string name="pref_title_edit_notification_settings">Fógraí</string> + <string name="visibility_direct">Díreach: Post chuig úsáideoirí luaite amháin</string> + <string name="visibility_private">Do Leantóirí Amháin: Postáil do do chuid leantóirí amháin</string> + <string name="visibility_unlisted">Neamhliostaithe: Ná taispeáin in amlínte poiblí</string> + <string name="visibility_public">Poiblí: Post chuig amlínte poiblí</string> + <string name="dialog_mute_hide_notifications">Folaigh fógraí</string> + <string name="dialog_block_warning">Bloc @%1$s\?</string> + <string name="mute_domain_warning_dialog_ok">Folaigh an fearann iomlán</string> + <string name="mute_domain_warning">An bhfuil tú cinnte gur mhaith leat gach %1$s a bhac\? Ní fheicfidh tú inneachar ón bhfearann sin in aon amlíne poiblí ar bith ná i do chuid fógraí. Bainfear do chuid leantóirí ón bhfearann sin.</string> + <string name="dialog_redraft_post_warning">Scrios agus athdhréachtaigh an postáil seo\?</string> + <string name="dialog_delete_post_warning">Scrios an postáil seo\?</string> + <string name="dialog_unfollow_warning">An cuntas seo a scaoileadh\?</string> + <string name="dialog_message_cancel_follow_request">An iarraidh seo a leanas a chúlghairm\?</string> + <string name="dialog_download_image">Íoslódáil</string> + <string name="dialog_message_uploading_media">Uaslódáil…</string> + <string name="dialog_title_finishing_media_upload">Uaslódáil Meáin Críochnaithe</string> + <string name="login_connection">Ag nascadh…</string> + <string name="label_header">Ceanntásc</string> + <string name="label_quick_reply">Freagra…</string> + <string name="search_no_results">Gan torthaí</string> + <string name="hint_search">Cuardaigh…</string> + <string name="hint_note">Bith</string> + <string name="hint_display_name">Ainm taispeána</string> + <string name="hint_content_warning">Rabhadh ábhair</string> + <string name="hint_compose">Cad atá ag tarlú\?</string> + <string name="hint_domain">Cén ásc\?</string> + <string name="confirmation_domain_unmuted">%1$s neamhcheangailte</string> + <string name="confirmation_unmuted">Úsáideoir gan trácht</string> + <string name="confirmation_unblocked">Úsáideoir gan bhac</string> + <string name="confirmation_reported">Seolta!</string> + <string name="send_media_to">Comhroinn meáin chuig…</string> + <string name="send_post_content_to">Comhroinn postáil chuig…</string> + <string name="send_post_link_to">Comhroinn URL na postála chuig…</string> + <string name="downloading_media">Meáin íoslódála</string> + <string name="download_media">Íoslódáil meáin</string> + <string name="action_share_as">Comhroinn mar …</string> + <string name="action_open_as">Oscail mar %1$s</string> + <string name="action_copy_link">Cóipeáil an nasc</string> + <string name="download_image">Ag íoslódáil %1$s</string> + <string name="action_open_media_n">Meáin oscailte #%1$d</string> + <string name="title_links_dialog">Naisc Ghréasáin</string> + <string name="action_open_faved_by">Taispeáin toghanna</string> + <string name="action_open_reblogged_by">Taispeáin athchraolta</string> + <string name="action_open_reblogger">Oscail údar an athchraolta</string> + <string name="action_mentions">Buaicphointí</string> + <string name="action_links">Naisc ghréasáin</string> + <string name="action_add_tab">Cuir Tab leis</string> + <string name="action_schedule_post">Sceidealaigh Postáil</string> + <string name="action_emoji_keyboard">Méarchlár Emoji</string> + <string name="action_content_warning">Rabhadh ábhair</string> + <string name="action_toggle_visibility">Infheictheacht postálacha</string> + <string name="action_access_scheduled_posts">Postálacha sceidealta</string> + <string name="action_access_drafts">Dréachtaí</string> + <string name="action_reject">Diúltaigh</string> + <string name="action_accept">Glac</string> + <string name="action_undo">Cealaigh</string> + <string name="action_edit_own_profile">Cuir in Eagar</string> + <string name="action_save">Sábháil</string> + <string name="action_open_drawer">Tarraiceán a oscailt</string> + <string name="action_hide_media">Folaigh na meáin</string> + <string name="action_mention">Luaigh</string> + <string name="action_unmute">Unmute</string> + <string name="action_mute">Balbhaigh</string> + <string name="action_share">Comhroinn</string> + <string name="action_photo_take">Tóg pictiúr</string> + <string name="action_add_poll">Cuir vótaíocht leis</string> + <string name="action_add_media">Cuir meáin leis</string> + <string name="action_open_in_web">Oscail sa bhrabhsálaí</string> + <string name="action_view_media">Meáin</string> + <string name="action_view_follow_requests">Lean Iarrataí</string> + <string name="action_view_domain_mutes">Fearainn i bhfolach</string> + <string name="action_view_blocks">Úsáideoirí blocáilte</string> + <string name="action_view_mutes">Úsáideoirí fuaim</string> + <string name="action_view_bookmarks">Leabharmharcanna</string> + <string name="action_view_favourites">Toghanna</string> + <string name="action_view_profile">Próifíl</string> + <string name="action_close">Dún</string> + <string name="action_retry">Atriail</string> + <string name="action_send_public">SÉID!</string> + <string name="action_send">SÉID</string> + <string name="action_delete_and_redraft">Scrios agus athdhréachtú</string> + <string name="action_delete">Scrios</string> + <string name="action_edit">Cuir in Eagar</string> + <string name="action_report">Tuairiscigh</string> + <string name="action_show_reblogs">Taispeáin athchraolta</string> + <string name="action_hide_reblogs">Folaigh athchraolta</string> + <string name="action_unblock">Ná bac</string> + <string name="action_block">Bac</string> + <string name="action_unfollow">Stop ag leanúint</string> + <string name="action_follow">Lean</string> + <string name="action_logout_confirm">An bhfuil tú cinnte gur mhaith leat logáil amach as an gcuntas %1$s\?</string> + <string name="action_compose">Cum</string> + <string name="action_more">Níos mó</string> + <string name="action_unfavourite">Bain togha</string> + <string name="action_bookmark">Leabharmharc</string> + <string name="action_favourite">Togh</string> + <string name="error_media_upload_type">Ní féidir an cineál comhaid sin a uaslódáil.</string> + <string name="error_compose_character_limit">Tá an stádas ró-fhada!</string> + <string name="error_retrieving_oauth_token">Theip ar chomhartha logála isteach a fháil.</string> + <string name="error_authorization_denied">Diúltaíodh údarú.</string> + <string name="error_authorization_unknown">Tharla earráid údaraithe neamhaitheanta.</string> + <string name="error_no_web_browser_found">Níorbh fhéidir brabhsálaí gréasáin a aimsiú le húsáid.</string> + <string name="error_invalid_domain">Fearann neamhbhailí iontráilte</string> + <string name="error_empty">Ní féidir leis seo a bheith folamh.</string> + <string name="error_network">Tharla earráid líonra! Seiceáil do nasc agus bain triail eile as!</string> + <string name="error_generic">Tharla earráid.</string> + <string name="title_lists">Liostaí</string> + <string name="action_lists">Liostaí</string> + <string name="about_title_activity">Faoi</string> + <string name="action_reset_schedule">Athshocraigh</string> + <string name="action_search">Cuardaigh</string> + <string name="action_edit_profile">Cuir próifíl in eagar</string> + <string name="action_view_account_preferences">Roghanna Cuntais</string> + <string name="action_view_preferences">Sainroghanna</string> + <string name="action_logout">Logáil Amach</string> + <string name="title_drafts">Dréachtaí</string> + <string name="title_favourites">Toghanna</string> + <string name="error_failed_app_registration">Theip ar fhíordheimhniú leis an ásc sin.</string> + <string name="link_whats_an_instance">Cad is ásc ann\?</string> + <string name="action_login">Logáil isteach le Mastodon</string> + <string name="post_media_images">Íomhánna</string> + <string name="post_share_link">Comhroinn nasc chuig postáil</string> + <string name="post_share_content">Comhroinn inneachar na postála</string> + <string name="about_tusky_account">Próifíl Tusky</string> + <string name="about_tusky_license">Is bogearraí foinse oscailte agus saor in aisce é Tusky. Tá sé ceadúnaithe faoi Leagan 3. Ceadúnas Poiblí Ginearálta GNU 3. Is féidir leat an ceadúnas a fheiceáil anseo: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="about_powered_by_tusky">Cumhachtaithe ag Tusky</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="description_account_locked">Cuntas faoi Ghlas</string> + <plurals name="notification_title_summary"> + <item quantity="other">%1$d idirghníomhaíochtaí nua</item> + </plurals> + <string name="notification_summary_small">%1$s agus %2$s</string> + <string name="notification_summary_medium">%1$s, %2$s, agus %3$s</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s agus %4$d cinn eile</string> + <string name="notification_mention_format">Luaigh %1$s tú</string> + <string name="notification_poll_description">Fógraí faoi pobalbhreitheanna a bhfuil deireadh leo</string> + <string name="notification_poll_name">Vótaí</string> + <string name="notification_favourite_description">Fógraí nuair a thoghtar do chuid postálacha</string> + <string name="notification_favourite_name">Toghanna</string> + <string name="notification_boost_description">Fógraí nuair a athchraoltar do chuid postálacha</string> + <string name="notification_boost_name">Athchraolta</string> + <string name="notification_follow_request_description">Fógraí faoi iarratais a leanúint</string> + <string name="notification_follow_request_name">Lean Iarrataí</string> + <string name="notification_follow_description">Fógraí faoi leantóirí nua</string> + <string name="notification_follow_name">Leantóirí Nua</string> + <string name="notification_mention_descriptions">Fógraí faoi luanna nua</string> + <string name="notification_mention_name">Tagairtí Nua</string> + <string name="post_text_size_largest">Is mó</string> + <string name="post_text_size_large">Mór</string> + <string name="post_text_size_medium">Meán-mhéid</string> + <string name="post_text_size_small">Beag</string> + <string name="post_text_size_smallest">Is bige</string> + <string name="pref_post_text_size">Méid an téacs postála</string> + <string name="post_privacy_followers_only">Do leantóirí amháin</string> + <string name="post_privacy_unlisted">Neamhliostaithe</string> + <string name="post_privacy_public">Poiblí</string> + <string name="pref_main_nav_position_option_bottom">Bun</string> + <string name="pref_main_nav_position_option_top">Barr</string> + <string name="pref_main_nav_position">Príomhshuíomh nascleanúna</string> + <string name="pref_failed_to_sync">Theip ar shocruithe a sync</string> + <string name="pref_publishing">Foilsitheoireacht (synced leis an bhfreastalaí)</string> + <string name="pref_default_media_sensitivity">Déan na meáin a mharcáil i gcónaí mar íogaire</string> + <string name="pref_default_post_privacy">Príobháideacht réamhshocraithe postálacha</string> + <string name="pref_title_http_proxy_port">Port seachfhreastalaí HTTP</string> + <string name="pref_title_http_proxy_server">Freastalaí seachfhreastalaí HTTP</string> + <string name="pref_title_http_proxy_enable">Cumasaigh seachfhreastalaí HTTP</string> + <string name="pref_title_http_proxy_settings">Seachfhreastalaí HTTP</string> + <string name="pref_title_proxy_settings">Seachfhreastalaí</string> + <string name="pref_title_show_media_preview">Íoslódáil réamhamharcanna meán</string> + <string name="pref_title_show_replies">Taispeáin freagraí</string> + <string name="pref_title_show_boosts">Taispeáin athchraolta</string> + <string name="pref_title_post_filter">Scagadh amlíne</string> + <string name="pref_title_gradient_for_media">Taispeáin grádáin ildaite do na meáin i bhfolach</string> + <string name="pref_title_animate_gif_avatars">Beochan abhatár GIF</string> + <string name="pref_title_bot_overlay">Taispeáin táscaire do róbónna</string> + <string name="pref_title_language">Teanga</string> + <string name="label_avatar">Abhatár</string> + <string name="title_mentions_dialog">Tráchtanna</string> + <string name="action_reblog">Athchraol</string> + <string name="action_unreblog">Cealaigh athchraoladh</string> + <string name="action_reply">Freagra</string> + <string name="action_quick_reply">Freagra Tapa</string> + <string name="report_comment_hint">Tuairimí Breise\?</string> + <string name="report_username_format">Tuairiscigh @%1$s</string> + <string name="notification_follow_request_format">D’iarr %1$s tú a leanúint</string> + <string name="notification_follow_format">lean %1$s thú</string> + <string name="notification_reblog_format">D\'athchraol %1$s do phostáil</string> + <string name="footer_empty">Níl aon rud anseo. Tarraingt anuas chun athnuachan a dhéanamh!</string> + <string name="message_empty">Níl aon rud anseo.</string> + <string name="post_content_show_less">Fill</string> + <string name="filter_dialog_whole_word_description">Nuair atá an eochairfhocal nó an frása alfa-uimhriúil amháin, ní chuirfear i bhfeidhm é ach má oireann sé don fhocal iomlán</string> + <string name="title_posts">Postálacha</string> + <string name="notification_favourite_format">Thogh %1$s do phostáil</string> + <string name="action_unmute_desc">Unmute %1$s</string> + <string name="action_mute_conversation">Comhrá tost</string> + <string name="action_hashtags">Clibeanna hash</string> + <string name="title_hashtags_dialog">Hashtags</string> + <string name="dialog_whats_an_instance">Is féidir an seoladh nó fearann a ghabhann le hásc ar bith a chur isteach anseo, mar shampla mastodon.social, icosahedron.website, social.tchncs.de, agus <a href="https://instances.social">níos mó!</a> +\n +\nMuna bhfuil cuntas agat fós, is féidir leat ainm an áisc ar mhaith leat a bheith páirteach ann a chur isteach, agus cuntas a chruthú ann. +\n +\nIs éard atá i gceist le hásc na láthair amháin ina ndéantar do chuntas a óstáil, ach is féidir leat cumarsáid a dhéanamh go héasca le daoine eile agus iad a leanúint ar áisc eile mar a bheadh sibh ar an suíomh céanna. +\n +\nIs féidir tuilleadh faisnéise a fháil ag <a href="https://joinmastodon.org"> joinmastodon.org </a>. </string> + <string name="about_project_site">Suíomh Gréasáin an tionscadail: +\n https://tusky.app</string> + <string name="about_bug_feature_request_site">Tuarascálacha ar fhabhtanna & iarratais ar ghnéithe: +\n https://github.com/tuskyapp/Tusky/issues</string> + <string name="post_media_video">Físeán</string> + <string name="state_follow_requested">Lean iarrtha</string> + <string name="follows_you">Leanann tú</string> + <string name="pref_title_alway_show_sensitive_media">Taispeáin ábhar íogair i gcónaí</string> + <string name="filter_dialog_update_button">Nuashonrú</string> + <string name="filter_dialog_whole_word">Focal iomlán</string> + <string name="filter_add_description">Frása le scagadh</string> + <string name="add_account_name">Cuir Cuntas leis</string> + <string name="pref_title_alway_open_spoiler">Leathnaigh i gcónaí postálacha atá marcáilte le rabhaidh ábhair</string> + <string name="title_media">Meáin</string> + <string name="replying_to">Ag freagairt do @%1$s</string> + <string name="load_more_placeholder_text">Lódáil a thuilleadh</string> + <string name="pref_title_thread_filter_keywords">Comhráite</string> + <string name="filter_addition_title">Cuir scagaire leis</string> + <string name="filter_edit_title">Cuir scagaire in eagar</string> + <string name="filter_dialog_remove_button">Bain</string> + <string name="pref_title_public_filter_keywords">Amlínte poiblí</string> + <string name="expand_collapse_all_posts">Leathnaigh/Fill na postálacha go léir</string> + <string name="restart_emoji">Beidh ort Tusky a atosú chun na hathruithe seo a chur i bhfeidhm</string> + <string name="caption_notoemoji">Sraith emoji reatha Google</string> + <string name="license_description">Tá cód agus sócmhainní ó na tionscadail foinse oscailte seo a leanas i Tusky:</string> + <string name="label_remote_account">Féadfaidh an fhaisnéis thíos próifíl an úsáideora a léiriú go neamhiomlán. Brúigh chun próifíl iomlán a oscailt sa bhrabhsálaí.</string> + <string name="description_poll">Vótaíocht le roghanna: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="list">Liosta</string> + <string name="compose_shortcut_long_label">Scríobh postáil</string> + <string name="notification_clear_text">An bhfuil tú cinnte gur mhaith leat do chuid fógraí go léir a ghlanadh go buan\?</string> + <string name="poll_ended_created">Tá deireadh le vótaíocht a chruthaigh tú</string> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d nóiméad fágtha</item> + <item quantity="two">%1$d nóiméid fágtha</item> + <item quantity="few">%1$d nóiméid fágtha</item> + <item quantity="many">%1$d nóiméid fágtha</item> + <item quantity="other">%1$d nóiméid fágtha</item> + </plurals> + <string name="failed_fetch_posts">Theip ar postálacha a ghabháil</string> + <string name="report_description_1">Seolfar an tuarascáil chuig do mhodhnóir freastalaí. Féadfaidh tú míniú a thabhairt ar an bhfáth go bhfuil tú ag tuairisciú an chuntais seo thíos:</string> + <string name="add_account_description">Cuir Cuntas Mastodon nua leis</string> + <string name="error_create_list">Níorbh fhéidir liosta a chruthú</string> + <string name="error_rename_list">Níorbh fhéidir an liosta a athainmniú</string> + <string name="error_delete_list">Níorbh fhéidir an liosta a scriosadh</string> + <string name="action_create_list">Cruthaigh liosta</string> + <string name="action_rename_list">Athainmnigh an liosta</string> + <string name="action_delete_list">Scrios an liosta</string> + <string name="hint_search_people_list">Cuardaigh daoine a leanann tú</string> + <string name="action_add_to_list">Cuir cuntas leis an liosta</string> + <string name="action_remove_from_list">Bain cuntas ón liosta</string> + <string name="compose_active_account_description">Postáil le cuntas %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">Déan cur síos ar dhaoine lagamhairc +\n(teorainn carachtar %1$d)</item> + </plurals> + <string name="action_set_caption">Socraigh fotheideal</string> + <string name="action_remove">Bain</string> + <string name="lock_account_label">Cuntas glasála</string> + <string name="lock_account_label_description">Éileofar ort leantóirí a cheadú de láimh</string> + <string name="compose_save_draft">Sábháil dréacht\?</string> + <string name="send_post_notification_title">Postáil á sheoladh…</string> + <string name="send_post_notification_error_title">Earráid le postáil a sheoladh</string> + <string name="send_post_notification_channel_name">Postálacha a Sheoladh</string> + <string name="send_post_notification_cancel_title">Cealaíodh seoladh</string> + <string name="send_post_notification_saved_content">Sábháladh cóip den phostáil i do chuid dréachtaí</string> + <string name="action_compose_shortcut">Cum</string> + <string name="error_no_custom_emojis">Níl emoji-nna saincheaptha ag d\'ásc %1$s</string> + <string name="emoji_style">Stíl Emoji</string> + <string name="system_default">Réamhshocrú an chórais</string> + <string name="download_fonts">Caithfear na tacair emoji seo a íoslódáil ar dtús</string> + <string name="performing_lookup_title">Amharc taibhithe…</string> + <string name="action_open_post">Oscail postáil</string> + <string name="restart_required">Atosú aip de dhíth</string> + <string name="later">Níos déanaí</string> + <string name="restart">Atosaigh</string> + <string name="caption_systememoji">Tacar emoji réamhshocraithe do ghléas</string> + <string name="caption_blobmoji">Na emojis Blob atá ar eolas ó Android 4.4-7.1</string> + <string name="caption_twemoji">Tacar emoji caighdeánach Mastodon</string> + <string name="download_failed">Theip ar íoslódáil</string> + <string name="profile_badge_bot_text">Bot</string> + <string name="account_moved_description">Tá %1$s tar éis bogadh go:</string> + <string name="reblog_private">Athchraol don bhunlucht léite</string> + <string name="license_apache_2">Ceadúnaithe faoin gCeadúnas Apache (cóip thíos)</string> + <string name="profile_metadata_label">Meiteashonraí próifíle</string> + <string name="profile_metadata_add">cuir sonraí leis</string> + <string name="profile_metadata_label_label">Lipéad</string> + <string name="profile_metadata_content_label">Ábhar</string> + <string name="pref_title_absolute_time">Úsáid am iomlán</string> + <string name="pin_action">Bioráin</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s </b> Togha</item> + <item quantity="two"><b>%1$s</b> Toghanna</item> + <item quantity="few"><b>%1$s</b> Toghanna</item> + <item quantity="many"><b>%1$s</b> Toghanna</item> + <item quantity="other"><b>%1$s</b> Toghanna</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> athchraoladh</item> + <item quantity="two"><b>%1$s</b> athchraolta</item> + <item quantity="few"><b>%1$s</b> athchraolta</item> + <item quantity="many"><b>%1$s</b> athchraolta</item> + <item quantity="other"><b>%1$s</b> athchraolta</item> + </plurals> + <string name="title_reblogged_by">Athchraolta ag</string> + <string name="title_favourited_by">Tofa ag</string> + <string name="conversation_2_recipients">%1$s agus %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s agus %3$d níos mó</string> + <string name="description_post_media">Meáin: %1$s</string> + <string name="description_post_cw">Rabhadh ábhair: %1$s</string> + <string name="description_post_media_no_description_placeholder">Gan cur síos</string> + <string name="description_post_favourited">Tofa</string> + <string name="description_post_bookmarked">Leabharmharcáilte</string> + <string name="description_visibility_public">Poiblí</string> + <string name="description_visibility_unlisted">Neamhliostaithe</string> + <string name="description_visibility_private">Leantóirí</string> + <string name="description_visibility_direct">Díreach</string> + <string name="hint_list_name">Ainm liosta</string> + <string name="add_hashtag_title">Cuir hashtag leis</string> + <string name="edit_hashtag_hint">Hashtag gan #</string> + <string name="select_list_title">Roghnaigh liosta</string> + <string name="notifications_clear">Glan</string> + <string name="notifications_apply_filter">Scagaire</string> + <string name="filter_apply">Cuir iarratas isteach</string> + <string name="compose_shortcut_short_label">Cum</string> + <string name="compose_preview_image_description">Gníomhartha maidir le híomhá %1$s</string> + <string name="poll_info_format"> <!-- 15 vóta • 1 uair fágtha --> %1$s •%2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s vóta</item> + <item quantity="two">%1$s vóta</item> + <item quantity="few">%1$s vóta</item> + <item quantity="many">%1$s vóta</item> + <item quantity="other">%1$s vóta</item> + </plurals> + <plurals name="poll_info_people"> + <item quantity="one">%1$s duine</item> + <item quantity="two">%1$s daoine</item> + <item quantity="few">%1$s daoine</item> + <item quantity="many">%1$s daoine</item> + <item quantity="other">%1$s daoine</item> + </plurals> + <string name="poll_info_time_absolute">foircinn ag %1$s</string> + <string name="poll_info_closed">dúnta</string> + <string name="poll_vote">Vóta</string> + <string name="poll_ended_voted">Tá deireadh le vótaíocht ar vótáil tú ann</string> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d lá</item> + <item quantity="two">%1$d lá</item> + <item quantity="few">%1$d lá</item> + <item quantity="many">%1$d lá</item> + <item quantity="other">%1$d lá</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">D\'imigh %1$d uair</item> + <item quantity="two">D\'imigh %1$d uair an chloig</item> + <item quantity="few">D\'imigh %1$d uair an chloig</item> + <item quantity="many">D\'imigh %1$d uair an chloig</item> + <item quantity="other">D\'imigh %1$d uair an chloig</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">D\'imigh %1$d soicind</item> + <item quantity="two">D\'imigh %1$d soicind</item> + <item quantity="few">D\'imigh %1$d soicind</item> + <item quantity="many">D\'imigh %1$d soicind</item> + <item quantity="other">D\'imigh %1$d soicind</item> + </plurals> + <string name="button_continue">Lean ar aghaidh</string> + <string name="button_back">Ar ais</string> + <string name="button_done">Déanta</string> + <string name="report_sent_success">Tuairiscíodh go rathúil @%1$s</string> + <string name="hint_additional_info">Tuairimí Breise</string> + <string name="report_remote_instance">Seol ar aghaidh chuig %1$s</string> + <string name="failed_report">Theip ar thuairisciú</string> + <string name="report_description_remote_instance">Is ó fhreastalaí eile an cuntas. Seol cóip gan ainm den tuarascáil ansin freisin\?</string> + <string name="title_accounts">Cuntais</string> + <string name="failed_search">Theip ar chuardach</string> + <string name="pref_title_enable_swipe_for_tabs">Cumasaigh gotha swipe aistriú idir cluaisíní</string> + <string name="create_poll_title">Vótaíocht</string> + <string name="duration_5_min">5 nóiméid</string> + <string name="duration_30_min">30 nóiméid</string> + <string name="duration_1_hour">1 uair an chloig</string> + <string name="duration_6_hours">6 uair an chloig</string> + <string name="duration_1_day">1 lá</string> + <string name="duration_3_days">3 lá</string> + <string name="duration_7_days">7 lá</string> + <string name="add_poll_choice">Cuir rogha leis</string> + <string name="poll_allow_multiple_choices">Ilroghanna</string> + <string name="poll_new_choice_hint">Rogha %1$d</string> + <string name="edit_poll">Cuir in Eagar</string> + <string name="post_lookup_error_format">Earráid agus an post á lorg %1$s</string> + <string name="no_drafts">Níl aon dréacht agat.</string> + <string name="no_scheduled_posts">Níl aon phostáil sceidealta agat.</string> + <string name="warning_scheduling_interval">Tá eatramh sceidealaithe íosta 5 nóiméad ag Mastodon.</string> + <string name="pref_title_show_cards_in_timelines">Taispeáin réamhamhairc nasc in amlínte</string> + <string name="pref_title_confirm_reblogs">Taispeáin dialóg dearbhaithe sula n-athchraolfar</string> + <string name="title_tab_preferences">Cluaisíní</string> + <string name="post_username_format">\@%1$s</string> + <string name="action_mute_domain">Balbhaigh %1$s</string> + <string name="action_unmute_domain">Unmute %1$s</string> + <string name="action_unmute_conversation">Comhrá unmute</string> + <string name="dialog_mute_warning">Tost @%1$s\?</string> + <string name="pref_title_post_tabs">Cluaisíní</string> + <string name="abbreviated_in_years">in %1$dy</string> + <string name="abbreviated_in_days">in %1$dd</string> + <string name="abbreviated_in_hours">in %1$dh</string> + <string name="abbreviated_in_minutes">in %1$dm</string> + <string name="abbreviated_in_seconds">in %1$ds</string> + <string name="abbreviated_years_ago">%1$dy</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="unreblog_private">Bain athchraoladh</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="unpin_action">Unpin</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="description_post_reblogged">Reblogged</string> + <string name="hashtags">Hashtags</string> + <string name="wellbeing_mode_notice">Folófar roinnt eolais ar féidir leis cuir isteach ar do mheabhairshláinte. Tá an méid seo a leanas san áireamh: +\n +\n - fógraí Toghanna/Athchraolta/Leanúna +\n - líon Toghanna/Athchraolta ar postálacha +\n - staitisticí Leantóirí/Postálacha ar phróifílí +\n +\n Ní chuirfear isteach ar brúfhógraí, ach is féidir leat do chuid sainroghanna phearsanra d\'fhógraí a athbhreithniú de láimh.</string> + <string name="error_loading_account_details">Theip ar sonraí an chuntais a lódáil</string> + <string name="instance_rule_title">Rialacha %1$s</string> + <string name="notification_subscription_name">Postálacha nua</string> + <string name="account_note_hint">Do nóta príobháideach faoin cuntas seo</string> + <string name="account_date_joined">Anseo ó %1$s</string> + <string name="notification_subscription_format">Tá %1$s tar éis postáil</string> + <string name="notification_update_format">Chuir %1$s postáil in eagar</string> + <string name="title_login">Logáil isteach</string> + <string name="notification_sign_up_format">Chláraigh %1$s</string> + <string name="action_unbookmark">Bain leabharmharc</string> + <string name="action_delete_conversation">Scrios comhrá</string> + <string name="action_dismiss">Dún</string> + <string name="action_details">Sonraí</string> + <string name="dialog_delete_conversation_warning">Scrios an comhrá seo\?</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-gd/strings.xml b/app/src/main/res/values-gd/strings.xml new file mode 100644 index 0000000..c7455f0 --- /dev/null +++ b/app/src/main/res/values-gd/strings.xml @@ -0,0 +1,689 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="title_lists">Liostaichean</string> + <string name="action_lists">Liostaichean</string> + <string name="about_title_activity">Mu dhèidhinn</string> + <string name="action_reset_schedule">Ath-shuidhich</string> + <string name="action_search">Lorg</string> + <string name="action_view_account_preferences">Roghainnean a’ chunntais</string> + <string name="action_view_preferences">Roghainnean</string> + <string name="action_logout">Clàraich a-mach</string> + <string name="title_drafts">Dreachdan</string> + <string name="title_favourites">Annsachdan</string> + <string name="link_whats_an_instance">Dè a th’ ann an ionstans\?</string> + <string name="edit_poll">Deasaich</string> + <string name="filter_edit_title">Deasaich a’ chriathrag</string> + <string name="pref_title_edit_notification_settings">Brathan</string> + <string name="action_edit_own_profile">Deasaich</string> + <string name="action_edit">Deasaich</string> + <string name="title_edit_profile">Deasaich a’ phròifil agad</string> + <string name="action_edit_profile">Deasaich a’ phròifil</string> + <string name="hint_display_name">Ainm-taisbeanaidh</string> + <string name="hint_content_warning">Rabhadh susbaint</string> + <string name="hint_compose">Dè tha dol\?</string> + <string name="hint_note">Sgeul-beatha</string> + <string name="label_quick_reply">Freagair…</string> + <string name="hint_search">Lorg…</string> + <string name="action_login">Clàraich a-steach le Tusky</string> + <string name="action_send_public">POSTAICH!</string> + <string name="action_emoji_keyboard">Meur-chlàr Emoji</string> + <string name="action_unfollow">Na lean tuilleadh</string> + <string name="action_follow">Lean</string> + <string name="action_more">Barrachd</string> + <string name="action_retry">Feuch ris a-rithist</string> + <string name="action_close">Dùin</string> + <string name="action_send">POSTAICH</string> + <string name="action_delete">Sguab às</string> + <string name="action_delete_and_redraft">Sguab às is dèan dreachd ùr air</string> + <string name="action_report">Dèan gearan</string> + <string name="action_unblock">Dì-bhac</string> + <string name="action_hide_reblogs">Falaich na brosnachaidhean</string> + <string name="action_show_reblogs">Seall na brosnachaidhean</string> + <string name="action_block">Bac</string> + <string name="action_compose">Sgrìobh</string> + <string name="action_content_warning">Rabhadh susbaint</string> + <string name="action_compose_shortcut">Sgrìobh</string> + <string name="compose_shortcut_short_label">Sgrìobh</string> + <string name="pref_title_show_boosts">Seall na brosnachaidhean</string> + <string name="action_open_reblogged_by">Seall na brosnachaidhean</string> + <string name="action_access_drafts">Dreachdan</string> + <string name="action_view_favourites">Annsachdan</string> + <string name="pref_title_notifications_enabled">Brathan</string> + <string name="title_notifications">Brathan</string> + <string name="notification_favourite_name">Annsachdan</string> + <string name="action_unsubscribe_account">Cuir crìoch air an fho-sgrìobhadh</string> + <string name="action_subscribe_account">Fo-sgrìobh</string> + <string name="pref_title_animate_custom_emojis">Beòthaich na h-Emojis gnàthaichte</string> + <string name="drafts_post_reply_removed">Bha againn ris a’ phost a bha thu airson freagairt dha a thoirt air falbh</string> + <string name="draft_deleted">Chaidh an dreach a sguabadh às</string> + <string name="drafts_failed_loading_reply">Cha deach leinn fiosrachadh na freagairte a luchdadh</string> + <string name="drafts_post_failed_to_send">Cha b’ urrainn dhuinn am post a chur!</string> + <string name="post_media_attachments">Ceanglachain</string> + <string name="post_media_audio">Fuaim</string> + <string name="dialog_delete_list_warning">A bheil thu cinnteach gu bheil thu airson an liosta %1$s a sguabadh às\?</string> + <string name="duration_7_days">7 làithean</string> + <string name="duration_3_days">3 làithean</string> + <string name="duration_1_day">Latha</string> + <string name="duration_6_hours">6 uairean a thìde</string> + <string name="duration_1_hour">Uair a thìde</string> + <string name="duration_30_min">Leth-uair a thìde</string> + <string name="duration_5_min">5 mionaidean</string> + <string name="duration_indefinite">Gun chrìoch</string> + <string name="label_duration">Faide</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Chan urrainn dhut barrachd air %1$d cheanglachan meadhain a luchdadh suas.</item> + <item quantity="two">Chan urrainn dhut barrachd air %1$d cheanglachan meadhain a luchdadh suas.</item> + <item quantity="few">Chan urrainn dhut barrachd air %1$d ceanglachain meadhain a luchdadh suas.</item> + <item quantity="other">Chan urrainn dhut barrachd air %1$d ceanglachan meadhain a luchdadh suas.</item> + </plurals> + <string name="wellbeing_hide_stats_profile">Falaich an stadastaireachd àireamhail air pròifilean</string> + <string name="wellbeing_hide_stats_posts">Falaich an stadastaireachd àireamhail air postaichean</string> + <string name="limit_notifications">Cuingich na brathan mun loidhne-ama</string> + <string name="review_notifications">Thoir sùil air na brathan</string> + <string name="wellbeing_mode_notice">Thèid cuid a dh’fhiosrachadh a dh’fhaodadh droch-bhuaidh a thoirt air d’ shlàinte-inntinn fhalach. Tha seo a’ gabhail a-staigh: +\n +\n - Brathan air annsachdan/brosnachaidhean/leanntainn +\n - Cunntas nan annsachdan/brosnachaidhean air postaichean +\n - Stadastaireachd an luchd-leantainn/nam postaichean air pròifilean +\n +\n Cha doir seo buaidh air na brathan-putaidh ach ’s urrainn dhut roghainnean nam brathan agad atharrachadh a làimh.</string> + <string name="pref_title_wellbeing_mode">Slàinte-inntinn</string> + <string name="notification_subscription_description">Brathan nuair a dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh post ùr</string> + <string name="notification_subscription_name">Postaichean ùra</string> + <string name="pref_title_notification_filter_subscriptions">dh’fhoillsich cuideigin air a rinn mi fo-sgrìobhadh post ùr</string> + <string name="notification_subscription_format">Phostaich %1$s rud</string> + <string name="no_announcements">Chan eil brath-fios ann.</string> + <string name="title_announcements">Brathan-fios</string> + <string name="account_note_saved">Chaidh a shàbhaladh!</string> + <string name="account_note_hint">Nòta prìobhaideach agad mun chunntas seo</string> + <string name="pref_title_hide_top_toolbar">Falaich tiotal a’ bhàir-inneal aig a’ bhàrr</string> + <string name="dialog_mute_hide_notifications">Falaich na brathan</string> + <string name="pref_title_confirm_reblogs">Seall dearbhadh mus dèan thu brosnachadh</string> + <string name="pref_title_show_cards_in_timelines">Seall ro-sheallaidhean air ceanglaichean sna loidhnichean-ama</string> + <string name="warning_scheduling_interval">Feumaidh co-dhiù 5 mionaidean a bhith eadar postaichean sgeidealaichte air Mastodon.</string> + <string name="no_scheduled_posts">Chan eil post sam bith air an sgeideal agad.</string> + <string name="no_drafts">Chan eil dreachd sam bith agad.</string> + <string name="post_lookup_error_format">Thachair mearachd le lorg a’ phuist %1$s</string> + <string name="poll_new_choice_hint">Roghainn %1$d</string> + <string name="poll_allow_multiple_choices">Iomadh roghainn</string> + <string name="add_poll_choice">Cuir roghainn ris</string> + <string name="create_poll_title">Cunntas-bheachd</string> + <string name="pref_title_enable_swipe_for_tabs">Cuir an comas gluasad grad-shlaighdidh airson leum a ghearradh o thaba gu taba</string> + <string name="failed_search">Dh’fhàillig leis an lorg</string> + <string name="title_accounts">Cunntasan</string> + <string name="report_description_remote_instance">Chaidh an cunntas a chlàradh air frithealaiche eile. A bheil thu airson lethbhreac dhen ghearan a chur dha-san gun ainm cuideachd\?</string> + <string name="report_description_1">Thèid do ghearan a chur gu maor an fhrithealaiche agad. ’S urrainn dhut mìneachadh a sholar air carson a tha thu a’ gearan mun chunntas gu h-ìosal:</string> + <string name="failed_fetch_posts">Cha b’ urrainn dhuinn na postaichean fhaighinn</string> + <string name="failed_report">Cha b’ urrainn dhuinn do ghearan a chlàradh</string> + <string name="report_remote_instance">Sìn air adhart gu %1$s</string> + <string name="hint_additional_info">Beachdan a bharrachd</string> + <string name="report_sent_success">Chaidh do gearan air @%1$s a chlàradh</string> + <string name="button_done">Deiseil</string> + <string name="button_back">Air ais</string> + <string name="button_continue">Air adhart</string> + <plurals name="poll_timespan_seconds"> + <item quantity="one">Tha %1$d diog air fhàgail</item> + <item quantity="two">Tha %1$d dhiog air fhàgail</item> + <item quantity="few">Tha %1$d diogan air fhàgail</item> + <item quantity="other">Tha %1$d diog air fhàgail</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">Tha %1$d mhionaid air fhàgail</item> + <item quantity="two">Tha %1$d mhionaid air fhàgail</item> + <item quantity="few">Tha %1$d mionaidean air fhàgail</item> + <item quantity="other">Tha %1$d mionaid air fhàgail</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">Tha %1$d uair a thìde air fhàgail</item> + <item quantity="two">Tha %1$d uair a thìde air fhàgail</item> + <item quantity="few">Tha %1$d uairean a thìde air fhàgail</item> + <item quantity="other">Tha %1$d uair a thìde air fhàgail</item> + </plurals> + <plurals name="poll_timespan_days"> + <item quantity="one">Tha %1$d latha air fhàgail</item> + <item quantity="two">Tha %1$d latha air fhàgail</item> + <item quantity="few">Tha %1$d làithean air fhàgail</item> + <item quantity="other">Tha %1$d latha air fhàgail</item> + </plurals> + <string name="poll_ended_created">Thàinig cunntas-bheachd sa chruthaich thu gu crìoch</string> + <string name="poll_ended_voted">Thàinig cunntas-bheachd sa bhòt thu gu crìoch</string> + <string name="poll_vote">Bhòt</string> + <string name="poll_info_closed">air a dhùnadh</string> + <string name="poll_info_time_absolute">thig e gu crìoch %1$s</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s duine</item> + <item quantity="two">%1$s dhuine</item> + <item quantity="few">%1$s dhaoine</item> + <item quantity="other">%1$s duine</item> + </plurals> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s bhòt</item> + <item quantity="two">%1$s bhòt</item> + <item quantity="few">%1$s bhòtaichean</item> + <item quantity="other">%1$s bhòt</item> + </plurals> + <string name="poll_info_format"> <!-- 15 bhòtaichean • 1 uair a thìde air fhàgail --> %1$s • %2$s</string> + <string name="compose_preview_image_description">Gnìomhan dhan dealbh %1$s</string> + <string name="notification_clear_text">A bheil thu cinnteach gu bheil thu airson na brathan uile agad fhalamhachadh gu buan\?</string> + <string name="compose_shortcut_long_label">Sgrìobh post</string> + <string name="filter_apply">Cuir an sàs</string> + <string name="notifications_apply_filter">Criathraich</string> + <string name="notifications_clear">Falamhaich</string> + <string name="list">Liosta</string> + <string name="select_list_title">Tagh liosta</string> + <string name="edit_hashtag_hint">Taga hais gun #</string> + <string name="add_hashtag_title">Cuir taga hais ris</string> + <string name="hint_list_name">Ainm na liosta</string> + <string name="description_poll">Cunntas-bheachd le roghainnean: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="description_visibility_direct">Dìreach</string> + <string name="description_post_bookmarked">’Na chomharra-lìn</string> + <string name="description_post_favourited">’Na annsachd</string> + <string name="description_post_reblogged">Air ath-bhlogadh</string> + <string name="description_post_media_no_description_placeholder">Gun tuairisgeul</string> + <string name="description_post_cw">Rabhadh susbainte: %1$s</string> + <string name="description_post_media">Meadhan: %1$s</string> + <string name="conversation_more_recipients">%1$s, %2$s ’s %3$d eile</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="title_favourited_by">’Na annsachd aig</string> + <string name="title_reblogged_by">’Ga brosnachadh le</string> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> bhrosnachadh</item> + <item quantity="two"><b>%1$s</b> bhrosnachadh</item> + <item quantity="few"><b>%1$s</b> brosnachaidhean</item> + <item quantity="other"><b>%1$s</b> brosnachadh</item> + </plurals> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> annsachd</item> + <item quantity="two"><b>%1$s</b> annsachd</item> + <item quantity="few"><b>%1$s</b> annsachdan</item> + <item quantity="other"><b>%1$s</b> annsachd</item> + </plurals> + <string name="pin_action">Prìnich</string> + <string name="unpin_action">Dì-phrìnich</string> + <string name="label_remote_account">Dh’fhaoidte nach fhaic thu pròifil gu lèir a’ chleachdaiche gu h-ìosal. Dèan brùthadh gus a’ phròifil shlàn fhosgladh ann am brabhsair.</string> + <string name="pref_title_absolute_time">Cleachd àm absaloideach</string> + <string name="profile_metadata_content_label">Susbaint</string> + <string name="profile_metadata_label_label">Leubail</string> + <string name="profile_metadata_add">cuir dàta ris</string> + <string name="profile_metadata_label">Meata-dàta na pròifile</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_apache_2">Fo cheadachas Apache License (chì thu lethbhreac dheth gu h-ìosal)</string> + <string name="license_description">Tha còs is maoin o na pròiseactan open source seo am broinn Tusky:</string> + <string name="unreblog_private">Na brosnaich tuilleadh</string> + <string name="reblog_private">Brosnaich dhan èisteachd thùsail</string> + <string name="account_moved_description">Chaidh %1$s a ghluasad gu:</string> + <string name="profile_badge_bot_text">Robotair</string> + <string name="download_failed">Dh’fhàillig an luchdadh a-nuas</string> + <string name="caption_notoemoji">Seata làithreach nan Emoji aig Google</string> + <string name="caption_twemoji">Seata stannardach nan Emoji aig Mastodon</string> + <string name="caption_blobmoji">Emojis Blob aig Android 4.4–7.1</string> + <string name="caption_systememoji">Seata bunaiteach nan Emojis air an uidheam agad</string> + <string name="restart">Ath-thòisich</string> + <string name="later">Uaireigin eile</string> + <string name="restart_emoji">Feumaidh tu Tusky ath-thòiseachadh gus na roghainnean seo a chur an sàs</string> + <string name="restart_required">Feumaidh tu an aplacaid ath-thòiseachadh</string> + <string name="action_open_post">Fosgail am post</string> + <string name="expand_collapse_all_posts">Leudaich/Co-theannaich gach post</string> + <string name="performing_lookup_title">’Ga lorg…</string> + <string name="download_fonts">Feumaidh tu na seataichean seo de dh’Emojis a luchdadh a-nuas an toiseach</string> + <string name="system_default">Bun-roghainn an t-siostaim</string> + <string name="emoji_style">Stoidhle nan Emojis</string> + <string name="error_no_custom_emojis">Chan eil Emojis gnàthaichte aig an ionstans %1$s agad</string> + <string name="send_post_notification_saved_content">Chaidh lethbhreac dhen phost agad a shàbhaladh ’na dhreachd</string> + <string name="send_post_notification_cancel_title">Chaidh sgur dhen chur</string> + <string name="send_post_notification_channel_name">A’ cur nam post</string> + <string name="send_post_notification_error_title">Mearachd a’ cur a’ phuist</string> + <string name="send_post_notification_title">A’ cur a’ phuist…</string> + <string name="compose_save_draft">A bheil thu airson a shàbhaladh ’na dhreachd\?</string> + <string name="lock_account_label_description">Feumaidh tu gabhail ri luchd-leantainn ùr a làimh</string> + <string name="lock_account_label">Glais an cunntas</string> + <string name="action_set_caption">Suidhich am fo-thiotal</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Mìnich an t-susbaint dhan fheadhainn air a bheil cion-lèirsinn (%1$d charactar air a char as fhaide)</item> + <item quantity="two">Mìnich an t-susbaint dhan fheadhainn air a bheil cion-lèirsinn (%1$d charactar air a char as fhaide)</item> + <item quantity="few">Mìnich an t-susbaint dhan fheadhainn air a bheil cion-lèirsinn (%1$d caractaran air a char as fhaide)</item> + <item quantity="other">Mìnich an t-susbaint dhan fheadhainn air a bheil cion-lèirsinn (%1$d caractar air a char as fhaide)</item> + </plurals> + <string name="compose_active_account_description">A’ postadh mar %1$s</string> + <string name="action_remove_from_list">Thoir an cunntas air falbh on liosta</string> + <string name="action_add_to_list">Cuir cunntas ris an liosta</string> + <string name="hint_search_people_list">Lorg daoine air a leanas tu</string> + <string name="action_delete_list">Sguab às an liosta</string> + <string name="action_rename_list">Ùraich an liosta</string> + <string name="action_create_list">Cruthaich liosta</string> + <string name="error_delete_list">Cha b’ urrainn dhuinn an liosta a sguabadh às</string> + <string name="error_rename_list">Cha b’ urrainn dhuinn an liosta ùrachadh</string> + <string name="error_create_list">Cha b’ urrainn dhuinn an liosta a chruthachadh</string> + <string name="add_account_description">Cuir cunntas Mastodon ùr ris</string> + <string name="add_account_name">Cuir cunntas ris</string> + <string name="filter_add_description">An abairt ri chriathradh</string> + <string name="filter_dialog_whole_word_description">Mur eil ach litrichean is àireamhan san fhacal-luirg, cha dèid a chur an sàs ach ma bhios e a’ maidseadh an fhacail shlàin</string> + <string name="pref_title_alway_open_spoiler">Leudaich postaichean ris a bheil rabhadh susbainte an-còmhnaidh</string> + <string name="post_share_link">Co-roinn ceangal dhan phost</string> + <string name="post_share_content">Co-roinn susbaint a’ phuist</string> + <string name="about_tusky_license">’S e bathar-bog saor le bun-tùs fosgailte a th’ ann an Tusky. Tha e fo cheadachas GNU General Public License tionndadh 3. Chì thu an ceadachas an-seo: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="notification_favourite_description">Brathan nuair a thèid post agad a chomharrachadh ’na annsachd</string> + <string name="notification_boost_description">Brathan nuair a thèid post agad brosnachadh</string> + <string name="dialog_redraft_post_warning">A bheil thu airson am post seo a sguabadh às is dreachd ùr a dhèanamh air\?</string> + <string name="dialog_delete_post_warning">A bheil thu airson am post seo a sguabadh às\?</string> + <string name="dialog_whats_an_instance">’S urrainn dhut seòladh no àrainn-lìn aig ionstans sam bith a chur a-steach an-seo, can mastodon.social, icosahedron.website, social.tchncs.de agus <a href="https://instances.social">a bharrachd!</a> +\n +\nMur eil cunntas agad fhathast, cuir a-steach ainm an ionstans sa bheil thu airson ballrachd fhaighinn airson cunntas a chruthachadh ann. +\n +\n’S e an t-aon àite far an cruthaich thu cunntas a th’ ann an ionstans ud ’s a nì an t-òstadh dhan chunntas agad. Gidheadh, ’s urrainn dhut conaltradh le daoine a tha air ionstans eile agus leantainn orra mar gun robh sibh air an aon làrach. +\n +\nGheibh thu barrachd fiosrachaidh air <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="send_post_content_to">Co-roinn am post le…</string> + <string name="send_post_link_to">Co-roinn URL a’ phuist le…</string> + <string name="action_schedule_post">Cuir post air an sgeideal</string> + <string name="action_toggle_visibility">Faicsinneachd a’ phuist</string> + <string name="action_access_scheduled_posts">Postaichean air an sgeideal</string> + <string name="notification_favourite_format">Is annsa le %1$s am post agad</string> + <string name="notification_reblog_format">Bhrosnaich %1$s am post agad</string> + <string name="title_scheduled_posts">Postaichean air an sgeideal</string> + <string name="title_view_thread">Snàithlean</string> + <string name="error_sender_account_gone">Mearachd a’ cur a’ phuist.</string> + <string name="action_unmute_desc">Dì-mhùch %1$s</string> + <string name="hashtags">Tagaichean hais</string> + <string name="description_visibility_private">Luchd-leantainn</string> + <string name="description_visibility_unlisted">Falaichte o liostaichean</string> + <string name="description_visibility_public">Poblach</string> + <string name="conversation_2_recipients">%1$s ’s %2$s</string> + <string name="action_remove">Thoir air falbh</string> + <string name="filter_dialog_whole_word">Facal slàn</string> + <string name="filter_dialog_update_button">Ùraich</string> + <string name="filter_dialog_remove_button">Thoir air falbh</string> + <string name="filter_addition_title">Cuir criathrag ris</string> + <string name="pref_title_thread_filter_keywords">Còmhraidhean</string> + <string name="pref_title_public_filter_keywords">Loidhnichean-ama poblach</string> + <string name="load_more_placeholder_text">luchdaich barrachd dheth</string> + <string name="replying_to">A’ freagairt gu @%1$s</string> + <string name="title_media">Meadhanan</string> + <string name="pref_title_alway_show_sensitive_media">Seall susbaint fhrionasach an-còmhnaidh</string> + <string name="follows_you">’Gad leantainn</string> + <string name="abbreviated_seconds_ago">%1$dd</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_hours_ago">%1$du</string> + <string name="abbreviated_days_ago">%1$dl</string> + <string name="abbreviated_years_ago">%1$db</string> + <string name="abbreviated_in_seconds">an ceann %1$dd</string> + <string name="abbreviated_in_minutes">an ceann %1$dm</string> + <string name="abbreviated_in_hours">an ceann %1$du</string> + <string name="abbreviated_in_days">an ceann %1$dl</string> + <string name="abbreviated_in_years">an ceann %1$db</string> + <string name="state_follow_requested">Iarrtas leantainn air</string> + <string name="post_media_video">Videothan</string> + <string name="post_media_images">Dealbhan</string> + <string name="about_tusky_account">Pròifil Tusky</string> + <string name="about_bug_feature_request_site">Aithrisean air bugaichean ⁊ iarrtasan air gleusan: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_project_site">Làrach-lìn a’ phròiseict: https://tusky.app</string> + <string name="about_powered_by_tusky">Le cumhachd Tusky</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="description_account_locked">Cunntas glaiste</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d chonaltradh ùr</item> + <item quantity="two">%1$d chonaltradh ùr</item> + <item quantity="few">%1$d conaltraidhean ùra</item> + <item quantity="other">%1$d conaltradh ùr</item> + </plurals> + <string name="notification_summary_small">%1$s ’s %2$s</string> + <string name="notification_summary_medium">%1$s, %2$s ’s %3$s</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s ’s %4$d eile</string> + <string name="notification_mention_format">Thug %1$s iomradh ort</string> + <string name="notification_poll_description">Brathan mu chunntasan-bheachd a thàinig gu crìoch</string> + <string name="notification_poll_name">Cunntasan-bheachd</string> + <string name="notification_boost_name">Brosnachaidhean</string> + <string name="notification_follow_request_description">Brathan mu iarrtasan leantainn</string> + <string name="notification_follow_request_name">Iarrtasan leantainn</string> + <string name="notification_follow_description">Brathan mu luchd-leantainn ùr</string> + <string name="notification_follow_name">Luchd-leantainn ùr</string> + <string name="notification_mention_descriptions">Brathan mu iomraidhean ùra</string> + <string name="notification_mention_name">Iomraidhean ùra</string> + <string name="post_text_size_largest">As motha</string> + <string name="post_text_size_large">Mòr</string> + <string name="post_text_size_medium">Meadhanach</string> + <string name="post_text_size_small">Beag</string> + <string name="post_text_size_smallest">As lugha</string> + <string name="pref_post_text_size">Meud teacsa nam post</string> + <string name="post_privacy_followers_only">Luchd-leantainn a-mhàin</string> + <string name="post_privacy_unlisted">Neo-liostaichte</string> + <string name="post_privacy_public">Poblach</string> + <string name="pref_main_nav_position_option_bottom">Aig a’ bhonn</string> + <string name="pref_main_nav_position_option_top">Aig a’ bhàrr</string> + <string name="pref_main_nav_position">Prìomh-ionad na seòladaireachd</string> + <string name="pref_failed_to_sync">Dh’fhàillig le sioncronachadh nan roghainnean</string> + <string name="pref_publishing">’Ga fhoillseachadh (ga shioncronachadh le frithealaiche)</string> + <string name="pref_default_media_sensitivity">Cuir comharra ri meadhanan an-còmhnaidh gu bheil iad frionasach</string> + <string name="pref_default_post_privacy">Prìobhaideachd bhunaiteach nam post</string> + <string name="pref_title_http_proxy_port">Port progsaidh HTTP</string> + <string name="pref_title_http_proxy_server">Frithealaiche progsaidh HTTP</string> + <string name="pref_title_http_proxy_enable">Cuir an comas a’ phrogsaidh HTTP</string> + <string name="pref_title_http_proxy_settings">Progsaidh HTTP</string> + <string name="pref_title_proxy_settings">Progsaidh</string> + <string name="pref_title_show_media_preview">Luchdaich a-nuas ro-sheallaidhean air meadhanan</string> + <string name="pref_title_show_replies">Seall na freagairtean</string> + <string name="pref_title_post_tabs">Tabaichean</string> + <string name="pref_title_post_filter">Criathradh na loidhne-ama</string> + <string name="pref_title_gradient_for_media">Seall caiseadan dathte an àite meadhanan falaichte</string> + <string name="pref_title_animate_gif_avatars">Beothaich avataran GIF</string> + <string name="pref_title_bot_overlay">Seall taisbeanair do bhotaichean</string> + <string name="pref_title_language">Cànan</string> + <string name="pref_title_custom_tabs">Cleachd tabaichean Chrome gnàthaichte</string> + <string name="pref_title_browser_settings">Brabhsair</string> + <string name="app_theme_system">Cleachd co-dhealbhachd an t-siostaim</string> + <string name="app_theme_auto">Gu fèin-obrachail aig beul na h-oidhche</string> + <string name="app_theme_black">Dubh</string> + <string name="app_theme_light">Soilleir</string> + <string name="app_them_dark">Dorcha</string> + <string name="pref_title_timeline_filters">Criathragan</string> + <string name="pref_title_timelines">Loidhnichean-ama</string> + <string name="pref_title_app_theme">Ùrlar na h-aplacaid</string> + <string name="pref_title_appearance_settings">Coltas</string> + <string name="pref_title_notification_filter_poll">thig cunntas-bheachd gu crìoch</string> + <string name="pref_title_notification_filter_favourites">thèid post agam a chur ris na h-annsachdan</string> + <string name="pref_title_notification_filter_reblogs">thèid post agam a bhrosnachadh</string> + <string name="pref_title_notification_filter_follow_requests">iarrar leantainn orm</string> + <string name="pref_title_notification_filter_follows">leanar orm</string> + <string name="pref_title_notification_filter_mentions">thoirear iomradh orm</string> + <string name="pref_title_notification_filters">Cuir brath thugam nuair a</string> + <string name="pref_title_notification_alert_light">Seall solas nuair a thig brath a-steach</string> + <string name="pref_title_notification_alert_vibrate">Dèan crith nuair a thig brath a-steach</string> + <string name="pref_title_notification_alert_sound">Seirm nuair a thig brath a-steach</string> + <string name="pref_title_notification_alerts">Rabhaidhean</string> + <string name="visibility_direct">Dìreach: Postaich dha na cleachdaichean le iomradh orra a-mhàin</string> + <string name="visibility_private">Luchd-leantainn a-mhàin: Postaich dhan luchd-leantainn a-mhàin</string> + <string name="visibility_unlisted">Neo-liostaichte: Na seall air loidhnichean-ama poblach</string> + <string name="visibility_public">Poblach: Postaich gu loidhnichean-ama poblach</string> + <string name="dialog_mute_warning">A bheil thu airson @%1$s a mhùchadh\?</string> + <string name="dialog_block_warning">A bheil thu airson @%1$s a bhacadh\?</string> + <string name="mute_domain_warning_dialog_ok">Falaich an àrainn uile gu lèir</string> + <string name="mute_domain_warning">A bheil thu cinnteach gu bheil thu airson %1$s a bhacadh uile gu lèir\? Chan fhaic thu susbaint on àrainn ud air loidhne-ama phoblach sam bith no am measg nam brathan agad. Thèid an luchd-leantainn agad on àrainn ud a thoirt air falbh.</string> + <string name="dialog_unfollow_warning">A bheil thu airson sgur de leantainn air a’ chunntas seo\?</string> + <string name="dialog_message_cancel_follow_request">A bheil thu airson an t-iarrtas leantainn a chùl-ghairm\?</string> + <string name="dialog_download_image">Luchdaich a-nuas</string> + <string name="dialog_message_uploading_media">’Ga luchdadh suas…</string> + <string name="dialog_title_finishing_media_upload">A’ crìochnachadh luchdadh suas meadhanan</string> + <string name="login_connection">A’ dèanamh ceangal…</string> + <string name="label_header">Bann-cinn</string> + <string name="label_avatar">Avatar</string> + <string name="search_no_results">Chan eil toradh ann</string> + <string name="hint_domain">Cò an t-ionstans\?</string> + <string name="confirmation_domain_unmuted">Chan eil %1$s falaichte tuilleadh</string> + <string name="confirmation_unmuted">Chaidh an cleachdaiche dhì-mhùchadh</string> + <string name="confirmation_unblocked">Chaidh an cleachdaiche a dhì-bhacadh</string> + <string name="confirmation_reported">Chaidh a chur!</string> + <string name="send_media_to">Co-roinn am meadhan le…</string> + <string name="downloading_media">A’ luchdadh a-nuas meadhanan</string> + <string name="download_media">Luchdaich a-nuas meadhanan</string> + <string name="action_share_as">Co-roinn mar …</string> + <string name="action_open_as">Fosgail mar %1$s</string> + <string name="action_copy_link">Dèan lethbhreac dhen cheangal</string> + <string name="download_image">A’ luchdadh a-nuas %1$s</string> + <string name="action_open_media_n">Fosgail meadhan #%1$d</string> + <string name="title_links_dialog">Ceanglaichean</string> + <string name="title_mentions_dialog">Iomraidhean</string> + <string name="title_hashtags_dialog">Tagaichean hais</string> + <string name="action_open_faved_by">Seall na h-annsachdan</string> + <string name="action_open_reblogger">Fosgail ùghdar a’ bhrosnachaidh</string> + <string name="action_hashtags">Tagaichean hais</string> + <string name="action_mentions">Iomraidhean</string> + <string name="action_links">Ceanglaichean</string> + <string name="action_add_tab">Cuir taba ris</string> + <string name="action_reject">Diùlt</string> + <string name="action_accept">Gabh ris</string> + <string name="action_undo">Neo-dhèan</string> + <string name="action_save">Sàbhail</string> + <string name="action_open_drawer">Fosgail an drathair</string> + <string name="action_hide_media">Falaich na meadhanan</string> + <string name="action_mention">Iomradh</string> + <string name="action_unmute_conversation">Dì-mhùch an còmhradh</string> + <string name="action_mute_conversation">Mùch an còmhradh</string> + <string name="action_unmute_domain">Dì-mhùch %1$s</string> + <string name="action_mute_domain">Mùch %1$s</string> + <string name="action_unmute">Dì-mhùch</string> + <string name="action_mute">Mùch</string> + <string name="action_share">Co-roinn</string> + <string name="action_photo_take">Tog dealbh</string> + <string name="action_add_poll">Cuir cunntas-bheachd ris</string> + <string name="action_add_media">Cuir meadhan ris</string> + <string name="action_open_in_web">Fosgail sa bhrabhsair</string> + <string name="action_view_media">Meadhanan</string> + <string name="action_view_follow_requests">Iarrtasan leantainn</string> + <string name="action_view_domain_mutes">Àrainnean falaichte</string> + <string name="action_view_blocks">Cleachdaichean bacte</string> + <string name="action_view_mutes">Cleachdaichean mùchte</string> + <string name="action_view_bookmarks">Comharran-lìn</string> + <string name="action_view_profile">Pròifil</string> + <string name="action_logout_confirm">A bheil thu cinnteach gu bheil thu airson clàradh a-mach à %1$s\? Thèid gach dàta ionadail a’ chunntais a sguabadh às, a’ gabhail a-staigh nan dreachdan is roghainnean.</string> + <string name="action_unfavourite">Thoir air falbh o na h-annsachdan</string> + <string name="action_bookmark">Cuir ris na comharran-lìn</string> + <string name="action_favourite">Cuir ris na h-annsachdan</string> + <string name="action_unreblog">Thoir am brosnachadh air falbh</string> + <string name="action_reblog">Brosnaich</string> + <string name="action_reply">Freagair</string> + <string name="action_quick_reply">Grad-fhreagairt</string> + <string name="report_comment_hint">Beachd sam bith eile\?</string> + <string name="report_username_format">Dèan gearan mu @%1$s</string> + <string name="notification_follow_request_format">Dh’iarr %1$s leantainn ort</string> + <string name="notification_follow_format">Lean %1$s ort</string> + <string name="footer_empty">Chan eil dad an-seo. Tarraing a-nuas airson ath-nuadhachadh!</string> + <string name="message_empty">Chan eil dad an-seo.</string> + <string name="post_content_show_less">Co-theannaich</string> + <string name="post_content_show_more">Leudaich</string> + <string name="post_content_warning_show_less">Seall nas lugha dheth</string> + <string name="post_content_warning_show_more">Seall barrachd dheth</string> + <string name="post_sensitive_media_directions">Briog air gus a shealltainn</string> + <string name="post_media_hidden_title">Meadhanan falaichte</string> + <string name="post_sensitive_media_title">Susbaint fhrionasach</string> + <string name="post_boosted_format">’Ga bhrosnachadh le %1$s</string> + <string name="post_username_format">\@%1$s</string> + <string name="title_licenses">Ceadachasan</string> + <string name="title_follow_requests">Iarrtasan leantainn</string> + <string name="title_domain_mutes">Àrainnean falaichte</string> + <string name="title_blocks">Cleachdaichean bacte</string> + <string name="title_mutes">Cleachdaichean mùchte</string> + <string name="title_bookmarks">Comharran-lìn</string> + <string name="title_followers">Luchd-leantainn</string> + <string name="title_follows">A’ leantainn air</string> + <string name="title_posts_pinned">Prìnichte</string> + <string name="title_posts_with_replies">Le freagairt</string> + <string name="title_posts">Postaichean</string> + <string name="title_tab_preferences">Tabaichean</string> + <string name="title_direct_messages">Teachdaireachdan dìreach</string> + <string name="title_public_federated">Co-naisgte</string> + <string name="title_public_local">Ionadail</string> + <string name="title_home">Dachaigh</string> + <string name="error_media_upload_sending">Dh’fhàillig leis an luchdadh suas.</string> + <string name="error_media_upload_image_or_video">Chan urrainn dhut an dà chuid dealbhan is videothan a cheangal ris an aon phost.</string> + <string name="error_media_download_permission">Tha feum air cead gus meadhanan a stòradh.</string> + <string name="error_media_upload_permission">Tha feum air cead gus meadhanan a leughadh.</string> + <string name="error_media_upload_opening">Cha b’ urrainn dhuinn am faidhle sin fhosgladh.</string> + <string name="error_media_upload_type">Cha ghabh an seòrsa de dh’fhaidhle seo a luchdadh suas.</string> + <string name="error_compose_character_limit">Tha am post ro fhada!</string> + <string name="error_retrieving_oauth_token">Cha deach leinn tòcan clàraidh a-steach fhaighinn. Ma mhaireas an duilgheadas seo, feuch “Clàraich a-steach le brabhsair” on chlàr-taice.</string> + <string name="error_authorization_denied">Chaidh an t-ùghdarrachadh a dhiùltadh. Ma tha thu cinnteach gun do chuir thu a-steach an teisteas ceart, feuch “Clàraich a-steach le brabhsair” on chlàr-taice.</string> + <string name="error_authorization_unknown">Thachair mearachd leis an ùghdarrachadh nach do dh’aithnich sinn. Ma mhaireas an duilgheadas seo, feuch “Clàraich a-steach le brabhsair” on chlàr-taice.</string> + <string name="error_no_web_browser_found">Cha do lorg sinn brabhsair-lìn a chleachdadh sinn.</string> + <string name="error_failed_app_registration">Dh’fhàillig leis an dearbhadh leis an ionstans ud. Ma mhaireas an duilgheadas seo, feuch “Clàraich a-steach le brabhsair” on chlàr-taice.</string> + <string name="error_invalid_domain">Chuir thu a-steach àrainn-lìn mì-dhligheach</string> + <string name="error_empty">Chan fhaod seo a bhith falamh.</string> + <string name="error_network">Thachair mearachd leis an lìonra. Thoir sùil air a’ cheangal agad is feuch ris a-rithist.</string> + <string name="error_generic">Thachair mearachd.</string> + <string name="follow_requests_info">Ged nach eil an cunntas agad glaiste, tha sgioba %1$s dhen bheachd gum b’ fheàirrde thu lèirmheas a dhèanamh air na h-iarrtasan leantainn o na cunntasan seo a làimh.</string> + <string name="dialog_delete_conversation_warning">A bheil thu airson an còmhradh seo a sguabadh às\?</string> + <string name="action_delete_conversation">Sguab às an còmhradh</string> + <string name="action_unbookmark">Thoir an comharra-lìn air falbh</string> + <string name="pref_title_confirm_favourites">Seall dearbhadh mus cuir thu post ris na h-annsachdan</string> + <string name="duration_30_days">30 latha</string> + <string name="duration_90_days">90 latha</string> + <string name="duration_180_days">180 latha</string> + <string name="duration_365_days">365 latha</string> + <string name="duration_14_days">14 làithean</string> + <string name="duration_60_days">60 latha</string> + <string name="tusky_compose_post_quicksetting_label">Sgrìobh post</string> + <string name="notification_sign_up_format">Chlàraich %1$s</string> + <string name="notification_sign_up_name">Clàraidhean</string> + <string name="notification_sign_up_description">Brathan mu cleachdaichean ùra</string> + <string name="pref_title_notification_filter_sign_ups">chlàraich cuideigin</string> + <string name="notification_update_format">Dheasaich %1$s am post aca</string> + <string name="notification_update_name">Deasachadh puist</string> + <string name="notification_update_description">Brathan nuair a thèid postaichean a rinn thu conaltradh leotha a dheasachadh</string> + <string name="pref_title_notification_filter_updates">chaidh post a rinn mi conaltradh leis a deasachadh</string> + <string name="title_login">Clàraich a-steach</string> + <string name="error_could_not_load_login_page">Cha b’ urrainn dhuinn duilleag a’ chlàraidh a-steach fhosgladh.</string> + <string name="saving_draft">A’ sàbhaladh na dreuchd…</string> + <string name="action_dismiss">Leig seachad</string> + <string name="action_details">Fiosrachadh</string> + <string name="account_date_joined">Air ballrachd fhaighinn %1$s</string> + <string name="tips_push_notification_migration">Clàraich a-steach às ùr leis a h-uile cunntas a chur na taice ri brathan putaidh an comas.</string> + <string name="title_migration_relogin">Clàraich a-steach às ùr airson brathan putaidh</string> + <string name="status_count_one_plus">1+</string> + <string name="action_edit_image">Deasaich an dealbh</string> + <string name="error_following_hashtag_format">Mearachd a’ leantainn air #%1$s</string> + <string name="error_multimedia_size_limit">Chan fhaod na faidhlichean video ’ fuaime a bhith nas motha na %1$s MB.</string> + <string name="error_unfollowing_hashtag_format">Mearachd a’ sgur de leantainn air #%1$s</string> + <string name="error_loading_account_details">Mearachd a’ luchdadh fiosrachadh a’ chunntais</string> + <string name="error_image_edit_failed">Cha b’ urrainn dhuinn an dealbh a dheasachadh.</string> + <string name="dialog_push_notification_migration">Airson brathan putaidh slighe UnifiedPush a chleachdadh, feumaidh Tusky cead airson fo-sgrìobhadh air brathan air an fhrithealaiche Mastodon agad fhaighinn. Bidh feum air clàradh a-steach às ùr airson na sgòpaichean OAuth a chaidh a cheadachadh dha Tusky atharrachadh. Ma nì thu clàradh a-steach às ùr an-seo no ann an “Roghainnean a’ chunntais”, cumaidh sinn na dreachdan is an tasgadan ionadail agad.</string> + <string name="dialog_push_notification_migration_other_accounts">Rinn thu clàradh a-steach às ùr dhan chunntas làithreach agad airson cead fo-sgrìobhadh putaidh a thoirt dha Tusky. Gidheadh, cha cunntasan eile agad fhathast nach deach imrich air an dòigh sin. Geàrr leum thuca is dèan clàradh a-steach às ùr do gach fear dhiubh airson taic do bhrathan UnifiedPush a chur an comas dhaibh.</string> + <string name="duration_no_change">(Gun atharrachadh)</string> + <string name="pref_title_show_self_username">Seall an t-ainm-cleachdaiche air na bàraichean-inneal</string> + <string name="set_focus_description">Thoir gogag no slaod an cearcall a thaghadh puing an fhòcais a chithear air na dealbhagan an-còmhnaidh.</string> + <string name="description_post_language">Cànan a’ phuist</string> + <string name="pref_show_self_username_always">An-còmhnaidh</string> + <string name="pref_show_self_username_disambiguate">Nuair a bhios iomadh cunntas air an clàradh a-steach</string> + <string name="pref_show_self_username_never">Chan ann idir</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="action_set_focus">Suidhich puing an fhòcais</string> + <string name="delete_scheduled_post_warning">A bheil thu airson am post sgeidealaichte seo a sguabadh às\?</string> + <string name="instance_rule_info">Le clàradh a-steach, bidh tu ag aontachadh ri riaghailtean %1$s.</string> + <string name="instance_rule_title">riaghailtean %1$s</string> + <string name="action_add_reaction">cuir freagairt ris</string> + <string name="failed_to_pin">Dh’fhàillig leis a’ phrìneachadh</string> + <string name="failed_to_unpin">Dh’fhàillig leis an dì-phrìneachadh</string> + <string name="compose_save_draft_loses_media">A bheil thu airson a shàbhaladh ’na dhreachd\? (Thèid na ceanglachain a luchdadh suas a-rithist nuair a dh’aisigeas tu an dreuchd.)</string> + <string name="a11y_label_loading_thread">A’ luchdadh an t-snàithlein</string> + <string name="pref_title_reading_order">Òrdugh an leughaidh</string> + <string name="pref_reading_order_oldest_first">As sine an toiseach</string> + <string name="pref_reading_order_newest_first">As ùire an toiseach</string> + <string name="action_unfollow_hashtag_format">A bheil thu airson sgur de #%1$s a leantainn\?</string> + <string name="mute_notifications_switch">Mùch na brathan</string> + <string name="hint_media_description_missing">Bu chòir do thuairisgeul a bhith aig a’ mheadhan.</string> + <string name="report_category_violation">Briseadh riaghailte</string> + <string name="report_category_spam">Spama</string> + <string name="report_category_other">Eile</string> + <string name="pref_summary_http_proxy_disabled">À comas</string> + <string name="pref_summary_http_proxy_missing"><cha deach a shuidheachadh></string> + <string name="pref_summary_http_proxy_invalid"><mì-dhligheach></string> + <string name="failed_to_remove_from_list">Cha deach leinn an cunntas a thoirt air falbh on liosta</string> + <string name="title_edits">Deasachaidhean</string> + <string name="status_created_at_now">an-dràsta</string> + <string name="pref_default_post_language">Cànan bunaiteach nam post</string> + <string name="status_created_info">Chruthaich %1$s</string> + <string name="status_edit_info">Dheasaich %1$s</string> + <string name="no_lists">Chan eil liosta sam bith agad.</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="pref_title_http_proxy_port_message">Bu chòir dhan phort a bhith eadar %1$d is %2$d</string> + <string name="error_following_hashtags_unsupported">Cha chuir an t-ionstans seo taic ri leantainn thagaichean hais.</string> + <string name="error_muting_hashtag_format">Mearachd a’ mùchadh #%1$s</string> + <string name="error_unmuting_hashtag_format">Mearachd a’ dì-mhùchadh #%1$s</string> + <string name="title_followed_hashtags">Tagaichean hais ’gan leantainn</string> + <string name="notification_report_name">Gearanan</string> + <string name="notification_report_description">Brathan mu ghearanan na maorsainneachd</string> + <string name="post_media_alt">ALT</string> + <string name="action_post_failed">Dh’fhàillig leis an luchdadh suas</string> + <string name="action_post_failed_detail_plural">Dh’fhàillig luchdadh suas nam postaichean agad is chaidh an sàbhaladh ’nan dreachdan. +\n +\nCha d’ fhuair sinn grèim air an fhrithealaiche no dhiùlt e na postaichean.</string> + <string name="action_post_failed_show_drafts">Seall na dreachdan</string> + <string name="action_post_failed_do_nothing">Leig seachad</string> + <string name="confirmation_hashtag_unfollowed">Chan eil #%1$s a’ leantainn tuilleadh</string> + <string name="description_post_edited">Chaidh a dheasachadh</string> + <string name="action_browser_login">Clàraich a-steach le brabhsair</string> + <string name="pref_title_notification_filter_reports">bhios gearan ùr ann</string> + <string name="action_add_or_remove_from_list">Cuir ris no thoir air falbh on liosta</string> + <string name="failed_to_add_to_list">Cha deach leinn an cunntas a chur ris an liosta</string> + <string name="description_login">Obraichidh seo mar as trice. Cha dèid dàta fhoillseachadh do dh’aplacaidean eile.</string> + <string name="description_browser_login">Dh’fhaoidte gun cuir seo taic ri dòighean dearbhaidh a bharrachd ach bi feum air brabhsair a chuireas taic ris.</string> + <string name="action_discard">Tilg air falbh na h-atharraichean</string> + <string name="action_continue_edit">Lean air an deasachadh</string> + <string name="post_edited">Air a dheasachadh %1$s</string> + <string name="notification_report_format">Gearan ùr air %1$s</string> + <string name="notification_header_report_format">Rinn %1$s gearan mu %2$s</string> + <string name="notification_summary_report_format">%1$s · Tha postaichean ris, %2$d dhiubh</string> + <string name="action_share_account_link">Co-roinn ceangal dhan chunntas</string> + <string name="account_username_copied">Chaidh lethbhreac a dhèanamh dhen ainm-chleachdaiche</string> + <string name="compose_unsaved_changes">Tha atharraichean gun sàbhaladh agad.</string> + <string name="error_status_source_load">Dh’fhàillig luchdadh bun-tùs a’ phuist on fhrithealaiche.</string> + <string name="action_post_failed_detail">Dh’fhàillig luchdadh suas a’ phuist agad is chaidh a shàbhaladh ’na dhreachd. +\n +\nCha d’ fhuair sinn grèim air an fhrithealaiche no dhiùlt e am post.</string> + <string name="action_share_account_username">Co-roinn ainm-cleachdaiche a’ chunntais</string> + <string name="send_account_link_to">Co-roinn URL a’ chunntais le…</string> + <string name="send_account_username_to">Co-roinn ainm-cleachdaiche a’ chunntais le…</string> + <string name="dialog_follow_hashtag_title">Lean an taga hais</string> + <string name="dialog_follow_hashtag_hint">#TagaHais</string> + <string name="post_media_image">Dealbh</string> + <string name="filter_action_warn">Thoir rabhadh</string> + <string name="filter_action_hide">Falaich</string> + <string name="action_refresh">Ath-nuadhaich</string> + <string name="notification_unknown_name">Neo-aithnichte</string> + <string name="socket_timeout_exception">Thug conaltradh ris an fhrithealaiche ro fhada</string> + <string name="ui_error_unknown">chan eil fhios dè an t-adhbhar</string> + <string name="ui_error_bookmark">Cha deach leinn comharra-lìn a chur ris a’ phost: %1$s</string> + <string name="ui_error_clear_notifications">Dh’fhàillig falamhachadh nam brathan: %1$s</string> + <string name="ui_error_favourite">Cha deach leinn am post a chur ris na h-annsachdan: %1$s</string> + <string name="ui_error_reblog">Dh’fhàillig brosnachadh a’ phuist: %1$s</string> + <string name="ui_error_vote">Dh’fhàillig bhòtadh sa chunntas-bheachd: %1$s</string> + <string name="ui_error_accept_follow_request">Dh’fhàillig le gabhail ris an iarrtas leantainn: %1$s</string> + <string name="ui_error_reject_follow_request">Dh’fhàillig le diùltadh an iarrtais leantainn: %1$s</string> + <string name="ui_success_accepted_follow_request">Chaidh gabhail ris an iarrtas leantainn</string> + <string name="ui_success_rejected_follow_request">Chaidh iarrtas leantainn a bhacadh</string> + <string name="select_list_manage">Stiùirich na liostaichean</string> + <string name="status_filtered_show_anyway">Seall e co-dhiù</string> + <string name="status_filter_placeholder_label_format">Criathraichte: %1$s</string> + <string name="pref_title_account_filter_keywords">Pròifilean</string> + <string name="hint_filter_title">A’ chriathrag agam</string> + <string name="filter_description_warn">Falaich le rabhadh</string> + <string name="filter_description_hide">Falaich uile gu lèir</string> + <string name="label_filter_action">Gnìomh na criathraige</string> + <string name="label_filter_context">Co-theacsaichean na criathraige</string> + <string name="label_filter_keywords">Faclan no abairtean-luirg rin criathradh</string> + <string name="action_add">Cuir ris</string> + <string name="filter_edit_keyword_title">Deasaich am facal-luirg</string> + <string name="title_public_trending_hashtags">Tagaichean hais a’ treandadh</string> + <string name="accessibility_talking_about_tag">Tha %1$d a’ bruidhinn mun taga hais %2$s</string> + <string name="total_usage">Cleachdadh iomlan</string> + <string name="total_accounts">Cunntasan iomlan</string> + <string name="pref_title_show_stat_inline">Seall stadastaireachd nam post san loidhne-ama</string> + <string name="filter_keyword_display_format">%1$s (facal slàn)</string> + <string name="filter_keyword_addition_title">Cuir facal-luirg ris</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="label_filter_title">Tiotal</string> + <string name="help_empty_home">Seo <b>loidhne-ama do dhachaigh</b>. Seallaidh i na postaichean o chionn goirid aig na cunntasan a leanas tu. +\n +\nAirson cunntasan a rùrachadh, lorg iad air tè dhe na loidhnichean-ama eile; mar eisimpleir, loidhne-ama ionadail an ionstans agad [iconics gmd_group]. Air neo lorg cunntasan a-rèir ainm [iconics gmd_search]; mar eisimpleir, lorg “Tusky” ach am faigh thu grèim air a’ chunntas againne air Mastodon.</string> + <string name="compose_delete_draft">A bheil thu airson an dreachd a sguabadh às\?</string> + <string name="load_newest_notifications">Luchdaich na brathan as ùire</string> + <string name="error_missing_edits">Tha fios aig an fhrithealaiche gun deach am post seo a dheasachadh ach chan eil lethbhreac dhen deasachadh aige-san is chan urrainn dhuinn a shealltainn dhut. +\n +\nSeo <a href="https://github.com/mastodon/mastodon/issues/25398">duilgheadas Mastodon #25398</a>.</string> + <string name="pref_ui_text_size">Meud teacsa na eadar-aghaidh</string> + <string name="notification_listenable_worker_name">Gnìomhachd sa chùlaibh</string> + <string name="notification_listenable_worker_description">Brathan nuair a bhios Tusky ag obair sa chùlaibh</string> + <string name="notification_notification_worker">A’ faighinn nam brathan…</string> + <string name="notification_prune_cache">Obair-ghlèidhidh air an tasgadan…</string> + <string name="about_device_info_title">An t-uidheam agad</string> + <string name="about_device_info">%1$s %2$s +\nTionndadh dhe Android: %3$s +\nTionndadh dhen SDK: %4$d</string> + <string name="about_account_info_title">An cunntas agad</string> + <string name="about_account_info">\@%1$s@%2$s +\nTionndadh: %3$s</string> + <string name="about_copy">Dèan lethbhreac de thionndadh is fiosrachadh an uidheim</string> + <string name="about_copied">Chaidh lethbhreac de thionndadh is fiosrachadh an uidheim a dhèanamh</string> + <string name="list_exclusive_label">Falaich o loidhne-ama na dachaigh</string> + <string name="error_media_upload_sending_fmt">Dh’fhàillig leis an luchdadh suas: %1$s</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml new file mode 100644 index 0000000..1862f08 --- /dev/null +++ b/app/src/main/res/values-gl/strings.xml @@ -0,0 +1,710 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="action_content_warning">Aviso sobre o contido</string> + <string name="action_toggle_visibility">Visibilidade da publicación</string> + <string name="action_access_scheduled_posts">Toots programados</string> + <string name="action_access_drafts">Borradores</string> + <string name="action_search">Buscar</string> + <string name="action_reject">Rexeitar</string> + <string name="action_accept">Aceptar</string> + <string name="action_undo">Desfacer</string> + <string name="action_edit_own_profile">Editar</string> + <string name="action_edit_profile">Editar perfil</string> + <string name="action_save">Gardar</string> + <string name="action_open_drawer">Abrir editor</string> + <string name="action_hide_media">Agochar multimedia</string> + <string name="action_mention">Mencionar</string> + <string name="action_unmute_conversation">Reactivar conversa</string> + <string name="action_mute_conversation">Acalar conversa</string> + <string name="action_unmute_domain">Reactivar %1$s</string> + <string name="action_mute_domain">Acalar %1$s</string> + <string name="action_unmute_desc">Reactivar %1$s</string> + <string name="action_unmute">Desacalar</string> + <string name="action_mute">Acalar</string> + <string name="action_share">Compartir</string> + <string name="action_photo_take">Facer foto</string> + <string name="action_add_poll">Engadir enquisa</string> + <string name="action_add_media">Engadir multimedia</string> + <string name="action_open_in_web">Abrir no navegador</string> + <string name="action_view_media">Multimedia</string> + <string name="action_view_follow_requests">Solicitudes de seguimento</string> + <string name="action_view_domain_mutes">Dominios agochados</string> + <string name="action_view_blocks">Usuarias bloqueadas</string> + <string name="action_view_mutes">Usuarias acaladas</string> + <string name="action_view_bookmarks">Marcadores</string> + <string name="action_view_favourites">Favoritos</string> + <string name="action_view_account_preferences">Preferencias da conta</string> + <string name="action_view_preferences">Preferencias</string> + <string name="action_view_profile">Perfil</string> + <string name="action_close">Pechar</string> + <string name="action_retry">Intenta outra vez</string> + <string name="action_send_public">TOOT!</string> + <string name="action_send">TOOT</string> + <string name="action_delete_and_redraft">Eliminar e reescribir</string> + <string name="action_delete">Eliminar</string> + <string name="action_edit">Editar</string> + <string name="action_report">Denunciar</string> + <string name="action_show_reblogs">Mostrar promocións</string> + <string name="action_hide_reblogs">Agochar promocións</string> + <string name="action_unblock">Desbloquear</string> + <string name="action_block">Bloquear</string> + <string name="action_unfollow">Deixar de seguir</string> + <string name="action_follow">Seguir</string> + <string name="action_logout_confirm">Tes a certeza de querer pechar a sesión %1$s\? Isto eliminará todos os datos locais da conta, incluíndo borradores e preferencias.</string> + <string name="action_logout">Pechar sesión</string> + <string name="action_login">Acceder con Tusky</string> + <string name="action_compose">Redactar</string> + <string name="action_more">Máis</string> + <string name="action_unfavourite">Eliminar favorito</string> + <string name="action_bookmark">Marcar</string> + <string name="action_favourite">Favorito</string> + <string name="action_unreblog">Eliminar promoción</string> + <string name="action_reblog">Promover</string> + <string name="action_reply">Responder</string> + <string name="action_quick_reply">Resposta rápida</string> + <string name="report_comment_hint">Comentarios adicionais\?</string> + <string name="report_username_format">Denunciar a @%1$s</string> + <string name="notification_subscription_format">%1$s publicou agora</string> + <string name="notification_follow_request_format">%1$s solicitou seguirte</string> + <string name="notification_follow_format">%1$s seguiute</string> + <string name="notification_favourite_format">%1$s fixo favorita a publicación</string> + <string name="notification_reblog_format">%1$s promoveu a túa publicación</string> + <string name="footer_empty">Nada por aquí. Arrastra hacia abaixo para actualizar!</string> + <string name="message_empty">Nada por aquí.</string> + <string name="post_content_show_less">Pregar</string> + <string name="post_content_show_more">Expandir</string> + <string name="post_content_warning_show_less">Ver menos</string> + <string name="post_content_warning_show_more">Ver máis</string> + <string name="post_sensitive_media_directions">Click para ver</string> + <string name="post_media_hidden_title">Multimedia agochado</string> + <string name="post_sensitive_media_title">Contido sensible</string> + <string name="post_boosted_format">%1$s promoveu</string> + <string name="post_username_format">\@%1$s</string> + <string name="title_licenses">Licenzas</string> + <string name="title_announcements">Anuncios</string> + <string name="title_scheduled_posts">Toots programados</string> + <string name="title_drafts">Borradores</string> + <string name="title_edit_profile">Edita o teu perfil</string> + <string name="title_follow_requests">Solicitudes de seguimento</string> + <string name="title_domain_mutes">Dominios agochados</string> + <string name="title_blocks">Usuarias bloqueadas</string> + <string name="title_mutes">Usuarias acaladas</string> + <string name="title_bookmarks">Marcadores</string> + <string name="title_favourites">Favoritos</string> + <string name="title_followers">Seguidoras</string> + <string name="title_follows">Segue</string> + <string name="title_posts_pinned">Fixado</string> + <string name="title_posts_with_replies">Con resposta</string> + <string name="title_posts">Publicacións</string> + <string name="title_view_thread">Fío</string> + <string name="title_tab_preferences">Pestanas</string> + <string name="title_direct_messages">Mensaxes directas</string> + <string name="title_public_federated">Federada</string> + <string name="title_public_local">Local</string> + <string name="title_notifications">Notificacións</string> + <string name="title_home">Inicio</string> + <string name="error_sender_account_gone">Erro ao enviar a publicación.</string> + <string name="error_media_upload_sending">Fallou a subida.</string> + <string name="error_media_upload_image_or_video">As imaxes e vídeo non poden engadirse simultáneamente á mesma publicación.</string> + <string name="error_media_download_permission">Requírese o permiso de almacenaxe do multimedia.</string> + <string name="error_media_upload_permission">Requírese o permiso de lectura do multimedia.</string> + <string name="error_media_upload_opening">Non se puido abrir o ficheiro.</string> + <string name="error_media_upload_type">Non pode subirse ese tipo de ficheiro.</string> + <string name="error_compose_character_limit">A publicación é demasiado longa!</string> + <string name="error_retrieving_oauth_token">Non se obtivo o token de acceso. Se non o consigues, inténtao desde Acceder no Navegador.</string> + <string name="error_authorization_denied">A autorización foi rexeitada. Se tes a certeza de que as credenciais son correctas, inténtao desde Acceder no Navegador no menú.</string> + <string name="error_authorization_unknown">Aconteceu un erro non identificado na autorización. Se persiste, inténtao desde Acceder no Navedor.</string> + <string name="error_no_web_browser_found">Non se atopou un navegador para utilizar.</string> + <string name="error_failed_app_registration">Fallou a autenticación nesta instancia. Se persiste, inténtao desde Acceder no Navegador no menú.</string> + <string name="error_invalid_domain">O dominio escrito non é válido</string> + <string name="error_empty">Esto non pode estar baleiro.</string> + <string name="error_network">Houbo un fallo na rede. Comproba a conexión e inténtao outra vez.</string> + <string name="error_generic">Algo fallou.</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="action_unsubscribe_account">Desubscribir</string> + <string name="action_subscribe_account">Subscribir</string> + <string name="drafts_post_reply_removed">Eliminouse o toot para o que redactaches a resposta</string> + <string name="duration_indefinite">Sen límite</string> + <string name="label_duration">Duración</string> + <string name="create_poll_title">Enquisa</string> + <string name="pref_title_enable_swipe_for_tabs">Activar xestos de desprazamento para moverse entre pestanas</string> + <string name="failed_search">Fallou a busca</string> + <string name="title_accounts">Contas</string> + <string name="report_description_remote_instance">A conta pertence a outro servidor. Queres enviar unha copia anónima da denuncia alí tamén\?</string> + <string name="report_description_1">A denuncia vaise enviar á moderación do teu servidor. Podes engadir algunha explicación ou razón pola que estás denunciando a conta:</string> + <string name="failed_fetch_posts">Fallou a obtención dos estados</string> + <string name="failed_report">Fallo ao realizar a denuncia</string> + <string name="report_remote_instance">Reenviar a %1$s</string> + <string name="hint_additional_info">Comentarios adicionais</string> + <string name="report_sent_success">Denuncia feita sobre @%1$s</string> + <string name="button_done">Feito</string> + <string name="button_back">Atrás</string> + <string name="button_continue">Continuar</string> + <plurals name="poll_timespan_seconds"> + <item quantity="one">queda %1$d segundo</item> + <item quantity="other">quedan %1$d segundos</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">queda %1$d minuto</item> + <item quantity="other">quedan %1$d minutos</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">queda %1$d hora</item> + <item quantity="other">quedan %1$d horas</item> + </plurals> + <plurals name="poll_timespan_days"> + <item quantity="one">queda %1$d día</item> + <item quantity="other">quedan %1$d días</item> + </plurals> + <string name="poll_ended_created">Rematou unha enquisa creada por ti</string> + <string name="poll_ended_voted">Rematou unha enquisa na que votaches</string> + <string name="poll_vote">Votar</string> + <string name="poll_info_closed">pechada</string> + <string name="poll_info_time_absolute">remata en %1$s</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s persoa</item> + <item quantity="other">%1$s persoas</item> + </plurals> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s voto</item> + <item quantity="other">%1$s votos</item> + </plurals> + <string name="poll_info_format"> \u0020<!-- 15 votos • queda 1 hora --> \u0020%1$s • %2$s</string> + <string name="compose_preview_image_description">Accións para a imaxe %1$s</string> + <string name="notification_clear_text">Tes a certeza de que queres borrar permanentemente todas as notificacións\?</string> + <string name="compose_shortcut_short_label">Redactar</string> + <string name="compose_shortcut_long_label">Redactar publicación</string> + <string name="filter_apply">Aplicar</string> + <string name="notifications_apply_filter">Filtrar</string> + <string name="notifications_clear">Borrar</string> + <string name="list">Listaxe</string> + <string name="select_list_title">Elexir listaxe</string> + <string name="hashtags">Cancelos</string> + <string name="edit_hashtag_hint">Cancelo sen #</string> + <string name="add_hashtag_title">Engadir cancelo</string> + <string name="hint_list_name">Nome da lista</string> + <string name="description_poll">Enquisa con opcións: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="description_visibility_direct">Directo</string> + <string name="description_visibility_private">Seguidoras</string> + <string name="description_visibility_unlisted">Non listado</string> + <string name="description_visibility_public">Público</string> + <string name="description_post_bookmarked">Marcado</string> + <string name="description_post_favourited">Favorecido</string> + <string name="description_post_reblogged">Repetido</string> + <string name="description_post_media_no_description_placeholder">Sen descrición</string> + <string name="description_post_cw">Aviso sobre o contido: %1$s</string> + <string name="description_post_media">Multimedia: %1$s</string> + <string name="conversation_more_recipients">%1$s, %2$s e %3$d máis</string> + <string name="conversation_2_recipients">%1$s e %2$s</string> + <string name="title_favourited_by">Favorecido por</string> + <string name="title_reblogged_by">Promovido por</string> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> Promoción</item> + <item quantity="other"><b>%1$s</b> Promocións</item> + </plurals> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Favorito</item> + <item quantity="other"><b>%1$s</b> Favoritos</item> + </plurals> + <string name="pin_action">Fixar</string> + <string name="unpin_action">Desafixar</string> + <string name="label_remote_account">A información inferior sobre a usuaria podería non estar completa. Preme para ver o perfil completo no navegador.</string> + <string name="pref_title_absolute_time">Usar hora absoluta</string> + <string name="profile_metadata_content_label">Contido</string> + <string name="profile_metadata_label_label">Etiqueta</string> + <string name="profile_metadata_add">engadir datos</string> + <string name="profile_metadata_label">Metadatos do perfil</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_apache_2">Con licenza Apache License (ver abaixo)</string> + <string name="license_description">Tusky ten código e ferramentas dos seguintes proxectos de código aberto:</string> + <string name="unreblog_private">Retirar promoción</string> + <string name="reblog_private">Promover para a audiencia orixinal</string> + <string name="account_moved_description">%1$s migrou a:</string> + <string name="profile_badge_bot_text">Bot</string> + <string name="download_failed">Fallou a descarga</string> + <string name="caption_notoemoji">Conxunto actual de emojis de Google</string> + <string name="caption_twemoji">Conxunto de emojis estándar en Mastodon</string> + <string name="caption_blobmoji">Os emojis Blob coñecidos de Android 4.4-7.1</string> + <string name="caption_systememoji">O conxunto de emojis por defecto no sistema</string> + <string name="restart">Reiniciar</string> + <string name="later">Máis tarde</string> + <string name="restart_emoji">Deberás reiniciar Tusky para aplicar os cambios</string> + <string name="restart_required">Require reiniciar app</string> + <string name="action_open_post">Abrir toot</string> + <string name="expand_collapse_all_posts">Expandir/Pregar tódolos estados</string> + <string name="performing_lookup_title">Realizando a busca…</string> + <string name="download_fonts">Deberás descargar primeiro estos conxuntos de emojis</string> + <string name="system_default">Por defecto no sistema</string> + <string name="emoji_style">Estilo dos emoji</string> + <string name="error_no_custom_emojis">A túa instancia %1$s non ten emojis personalizados</string> + <string name="action_compose_shortcut">Redactar</string> + <string name="send_post_notification_saved_content">Gardouse unha copia do toot nos borradores</string> + <string name="send_post_notification_cancel_title">Envío cancelado</string> + <string name="send_post_notification_channel_name">Enviando Toots</string> + <string name="send_post_notification_error_title">Erro ao enviar o toot</string> + <string name="send_post_notification_title">Enviando Toot…</string> + <string name="compose_save_draft">Gardar borrador\?</string> + <string name="lock_account_label_description">Require que aprobes manualmente as seguidoras</string> + <string name="lock_account_label">Bloquear conta</string> + <string name="action_remove">Eliminar</string> + <string name="action_set_caption">Escribir descrición</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Describe para persoas con deficiencias visuais (límite %1$d caracter)</item> + <item quantity="other">Describe para persoas con deficiencias visuais (%1$d caracteres como máximo)</item> + </plurals> + <string name="compose_active_account_description">Publicar como %1$s</string> + <string name="action_remove_from_list">Eliminar conta da listaxe</string> + <string name="action_add_to_list">Engadir conta á listaxe</string> + <string name="hint_search_people_list">Atopar persoas ás que segues</string> + <string name="action_delete_list">Eliminar a listaxe</string> + <string name="action_rename_list">Actualizar a listaxe</string> + <string name="action_create_list">Crear unha listaxe</string> + <string name="error_delete_list">Non se puido eliminar a listaxe</string> + <string name="error_rename_list">Non se actualizou a listaxe</string> + <string name="error_create_list">Non se puido crear a listaxe</string> + <string name="title_lists">Listaxes</string> + <string name="action_lists">Listaxes</string> + <string name="add_account_description">Engadir unha nova conta Mastodon</string> + <string name="add_account_name">Engadir conta</string> + <string name="filter_add_description">Frase a filtrar</string> + <string name="filter_dialog_whole_word_description">Cando a palabra ou frase é só alfanumérica, só se aplicará se concorda a palabra completa</string> + <string name="filter_dialog_whole_word">Palabra completa</string> + <string name="filter_dialog_update_button">Actualiza</string> + <string name="filter_dialog_remove_button">Eliminar</string> + <string name="filter_edit_title">Editar filtro</string> + <string name="filter_addition_title">Engadir filtro</string> + <string name="pref_title_thread_filter_keywords">Conversas</string> + <string name="pref_title_public_filter_keywords">Cronoloxías públicas</string> + <string name="load_more_placeholder_text">cargar máis</string> + <string name="replying_to">Respondendo a @%1$s</string> + <string name="title_media">Multimedia</string> + <string name="pref_title_alway_open_spoiler">Despregar sempre publicacións marcadas con avisos sobre o contido</string> + <string name="pref_title_alway_show_sensitive_media">Mostrar sempre contido sensible</string> + <string name="follows_you">Séguete</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_years_ago">%1$da</string> + <string name="abbreviated_in_seconds">fai %1$ds</string> + <string name="abbreviated_in_hours">fai %1$dh</string> + <string name="abbreviated_in_minutes">fai %1$dm</string> + <string name="abbreviated_in_days">en %1$dd</string> + <string name="abbreviated_in_years">en %1$da</string> + <string name="state_follow_requested">Seguimento solicitado</string> + <string name="post_media_attachments">Anexos</string> + <string name="post_media_audio">Audio</string> + <string name="post_media_video">Vídeo</string> + <string name="post_media_images">Imaxes</string> + <string name="post_share_link">Compartir ligazón ao toot</string> + <string name="post_share_content">Compartir contido do toot</string> + <string name="about_tusky_account">Perfil de Tusky</string> + <string name="about_bug_feature_request_site">Informar de fallos e solicitar funcións: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_project_site">Web do proxecto: https://tusky.app</string> + <string name="about_tusky_license">Tusky é software libre e de código aberto. Está baixo a licenza GNU General Public License Version 3. Podes ver a licenza aquí: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="about_powered_by_tusky">Desenvolta por Tusky</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_title_activity">Acerca de</string> + <string name="description_account_locked">Conta bloqueada</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d nova interacción</item> + <item quantity="other">%1$d novas interaccións</item> + </plurals> + <string name="notification_summary_small">%1$s e %2$s</string> + <string name="notification_summary_medium">%1$s, %2$s, e %3$s</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s e %4$d outras</string> + <string name="notification_mention_format">%1$s mencionoute</string> + <string name="notification_subscription_description">Notificacións cando alguén a quen estás subscrita publica novo contido</string> + <string name="notification_subscription_name">Novas publicacións</string> + <string name="notification_poll_description">Notificacións cando rematan as enquisas</string> + <string name="notification_poll_name">Enquisas</string> + <string name="notification_favourite_description">Notificacións cando as túas publicacións son favorecidas</string> + <string name="notification_favourite_name">Favoritos</string> + <string name="notification_boost_description">Notificacións cando as túas publicacións sexan promovidas</string> + <string name="notification_boost_name">Promocións</string> + <string name="notification_follow_request_description">Notificación acerca de solicitudes de seguimento</string> + <string name="notification_follow_request_name">Solicitudes de seguimento</string> + <string name="notification_follow_description">Notificacións acerca de novas seguidoras</string> + <string name="notification_follow_name">Novas seguidoras</string> + <string name="notification_mention_descriptions">Notificación de novas mencións</string> + <string name="notification_mention_name">Novas mencións</string> + <string name="post_text_size_largest">O máis grande</string> + <string name="post_text_size_large">Grande</string> + <string name="post_text_size_medium">Medio</string> + <string name="post_text_size_small">Pequeno</string> + <string name="post_text_size_smallest">O máis pequeno</string> + <string name="pref_post_text_size">Tamaño do texto do estado</string> + <string name="post_privacy_followers_only">Só seguidoras</string> + <string name="post_privacy_unlisted">Non listado</string> + <string name="post_privacy_public">Público</string> + <string name="pref_main_nav_position_option_bottom">Abaixo</string> + <string name="pref_main_nav_position_option_top">Arriba</string> + <string name="pref_main_nav_position">Posición de navegación principal</string> + <string name="pref_failed_to_sync">Fallou a sincr. das preferencis</string> + <string name="pref_publishing">Ao publicar (sincronizado co servidor)</string> + <string name="pref_default_media_sensitivity">Marcar sempre multimedia como sensible</string> + <string name="pref_default_post_privacy">Privacidade por defecto da publicación</string> + <string name="pref_title_http_proxy_port">Porto proxy HTTP</string> + <string name="pref_title_http_proxy_server">Servidor proxy HTTP</string> + <string name="pref_title_http_proxy_enable">Activar proxy HTTP</string> + <string name="pref_title_http_proxy_settings">Proxy HTTP</string> + <string name="pref_title_proxy_settings">Proxy</string> + <string name="pref_title_show_media_preview">Descarga vista previa do multimedia</string> + <string name="pref_title_show_replies">Mostar respostas</string> + <string name="pref_title_show_boosts">Mostrar promocións</string> + <string name="pref_title_post_tabs">Cronoloxía de Inicio</string> + <string name="pref_title_post_filter">Filtros na cronoloxía</string> + <string name="pref_title_animate_custom_emojis">Animar emojis personalizados</string> + <string name="pref_title_gradient_for_media">Mostra gradientes coloridos para multimedia oculto</string> + <string name="pref_title_animate_gif_avatars">Animar avatares GIF</string> + <string name="pref_title_bot_overlay">Mostra marca para bots</string> + <string name="pref_title_language">Idioma</string> + <string name="pref_title_custom_tabs">Usar pestanas personalizadas de Chrome</string> + <string name="pref_title_browser_settings">Navegador</string> + <string name="app_theme_system">Usar deseño do sistema</string> + <string name="app_theme_auto">Automático ao solpor</string> + <string name="app_theme_black">Negro</string> + <string name="app_theme_light">Claro</string> + <string name="app_them_dark">Escuro</string> + <string name="pref_title_timeline_filters">Filtros</string> + <string name="pref_title_timelines">Cronoloxías</string> + <string name="pref_title_app_theme">Decorado da app</string> + <string name="pref_title_appearance_settings">Aparencia</string> + <string name="pref_title_notification_filter_subscriptions">alguén a quen eu siga publique novo contido</string> + <string name="pref_title_notification_filter_poll">rematen as enquisas</string> + <string name="pref_title_notification_filter_favourites">marquen un toot meu como favorito</string> + <string name="pref_title_notification_filter_reblogs">promocionen un dos meus toots</string> + <string name="pref_title_notification_filter_follow_requests">soliciten seguirme</string> + <string name="pref_title_notification_filter_follows">me sigan</string> + <string name="pref_title_notification_filter_mentions">me mencionen</string> + <string name="pref_title_notification_filters">Notifícame cando</string> + <string name="pref_title_notification_alert_light">Notificar coa luz</string> + <string name="pref_title_notification_alert_vibrate">Notificar con vibración</string> + <string name="pref_title_notification_alert_sound">Nofiticar cun ton</string> + <string name="pref_title_notification_alerts">Alertas</string> + <string name="pref_title_notifications_enabled">Notificacións</string> + <string name="pref_title_edit_notification_settings">Notificacións</string> + <string name="visibility_direct">Directo: Visible só polas persoas mencionadas</string> + <string name="visibility_private">Só seguidoras: visible só polas seguidoras</string> + <string name="visibility_unlisted">Non listado: non mostrar en cronoloxías públicas</string> + <string name="visibility_public">Público: Publicar en cronoloxías públicas</string> + <string name="dialog_mute_hide_notifications">Agochar notificacións</string> + <string name="dialog_mute_warning">Acalar a @%1$s\?</string> + <string name="dialog_block_warning">Bloquear @%1$s\?</string> + <string name="mute_domain_warning_dialog_ok">Agochar todo o dominio</string> + <string name="mute_domain_warning">Tes a certeza de querer bloquear a todo %1$s\? Non verás o contido dese dominio en ningunha cronoloxía pública ou nas notificacións. As túas seguidoras nese dominio serán eliminadas.</string> + <string name="dialog_redraft_post_warning">Eliminar e reescribir esta publicación\?</string> + <string name="dialog_delete_post_warning">Eliminar esta publicación\?</string> + <string name="dialog_unfollow_warning">Deixar de seguir esta conta\?</string> + <string name="dialog_message_cancel_follow_request">Revogar a solicitude de seguimento\?</string> + <string name="dialog_download_image">Descargar</string> + <string name="dialog_message_uploading_media">Subindo…</string> + <string name="dialog_title_finishing_media_upload">Rematando a subida multimedia</string> + <string name="dialog_whats_an_instance">Aquí podes escribir o enderezo ou dominio de calquera instancia, como mastodon.social, icosahedron.website, social.techncs.de, e <a href="https://instances.social">máis!</a> +\n +\nSe aínda non tes unha conta, podes escribir o nome da instancia á que desexas unirte e crear unha conta nela. +\n +\nUnha instancia é o lugar onde se hospeda a túa conta, pero podes comunicarte facilmente e seguir a persoas noutras instancias como se estiveses alí. +\n +\nPodes atopar máis información en <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="login_connection">Conectando…</string> + <string name="link_whats_an_instance">Que é unha instancia\?</string> + <string name="label_header">Cabeceira</string> + <string name="label_avatar">Avatar</string> + <string name="label_quick_reply">Responder…</string> + <string name="search_no_results">Sen resultados</string> + <string name="hint_search">Buscar…</string> + <string name="hint_note">Acerca de</string> + <string name="hint_display_name">Nome público</string> + <string name="hint_content_warning">Aviso sobre o contido</string> + <string name="hint_compose">Que contas\?</string> + <string name="draft_deleted">Borrador eliminado</string> + <string name="drafts_failed_loading_reply">Fallou a carga da información da Resposta</string> + <string name="drafts_post_failed_to_send">Fallou o envío do toot!</string> + <string name="dialog_delete_list_warning">Tes a certeza de querer eliminar a listaxe %1$s\?</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Non podes subir máis de %1$d anexo multimedia.</item> + <item quantity="other">Non podes subir máis de %1$d anexos multimedia.</item> + </plurals> + <string name="wellbeing_hide_stats_profile">Agochar estatísticas cuantitativas nos perfís</string> + <string name="wellbeing_hide_stats_posts">Agochar estatísticas cuantitativas nas publicacións</string> + <string name="limit_notifications">Limitar notificacións da cronoloxía</string> + <string name="review_notifications">Revisar Notificacións</string> + <string name="wellbeing_mode_notice">Agocharemos algunha información que podería afectar ao teu benestar mental. Esto inclúe: +\n +\n- Notificacións acerca de Favoritos/Promocións/Seguimentos +\n- Número de Favoritos/Promocións nas publicacións +\n- Estatísticas de Seguidoras/Publicacións nos perfís +\n +\nAs notificacións tipo push non estarán afectadas, mais podes revisar as preferencias de notificacións manualmente.</string> + <string name="account_note_saved">Gardado!</string> + <string name="account_note_hint">Nota privada acerca desta conta</string> + <string name="pref_title_wellbeing_mode">Benestar</string> + <string name="pref_title_hide_top_toolbar">Agochar o título da barra de ferramentas superior</string> + <string name="pref_title_confirm_reblogs">Pedir confirmación antes de promover</string> + <string name="pref_title_show_cards_in_timelines">Ver vista previa das ligazóns nas cronoloxías</string> + <string name="warning_scheduling_interval">Mastodon ten un intervalo mínimo de 5 minutos para as programacións.</string> + <string name="no_announcements">Non hai anuncios.</string> + <string name="no_scheduled_posts">Non tes estados programados.</string> + <string name="no_drafts">Non tes borradores.</string> + <string name="post_lookup_error_format">Erro ao buscar publicación %1$s</string> + <string name="edit_poll">Editar</string> + <string name="poll_new_choice_hint">Opción %1$d</string> + <string name="poll_allow_multiple_choices">Múltiples opcións</string> + <string name="add_poll_choice">Engadir opción</string> + <string name="duration_7_days">7 días</string> + <string name="duration_3_days">3 días</string> + <string name="duration_1_day">1 día</string> + <string name="duration_6_hours">6 horas</string> + <string name="duration_1_hour">1 hora</string> + <string name="duration_30_min">30 minutos</string> + <string name="duration_5_min">5 minutos</string> + <string name="hint_domain">En que instancia\?</string> + <string name="confirmation_domain_unmuted">%1$s visible</string> + <string name="confirmation_unmuted">Usuaria reactivada</string> + <string name="confirmation_unblocked">Usuaria desbloqueada</string> + <string name="confirmation_reported">Enviado!</string> + <string name="send_media_to">Compartir multimedia en…</string> + <string name="send_post_content_to">Compartir toot en…</string> + <string name="send_post_link_to">Compartir URL do toot a…</string> + <string name="downloading_media">Descargando multimedia</string> + <string name="download_media">Descargar multimedia</string> + <string name="action_share_as">Compartir como …</string> + <string name="action_open_as">Abrir como %1$s</string> + <string name="action_copy_link">Copiar ligazón</string> + <string name="download_image">Descargando %1$s</string> + <string name="action_open_media_n">Abrir multimedia #%1$d</string> + <string name="title_links_dialog">Ligazóns</string> + <string name="title_mentions_dialog">Mencións</string> + <string name="title_hashtags_dialog">Cancelos</string> + <string name="action_open_faved_by">Mostrar favoritos</string> + <string name="action_open_reblogged_by">Mostrar promocións</string> + <string name="action_open_reblogger">Abrir autor da promoción</string> + <string name="action_hashtags">Cancelos</string> + <string name="action_mentions">Mencións</string> + <string name="action_links">Ligazóns</string> + <string name="action_add_tab">Engadir pestana</string> + <string name="action_reset_schedule">Restablecer</string> + <string name="action_schedule_post">Programar Toot</string> + <string name="action_emoji_keyboard">Teclado Emoji</string> + <string name="follow_requests_info">Aínda que a túa conta non está bloqueada, a administración de %1$s opina que debes revisar manualmente as peticións de seguimento destas contas.</string> + <string name="dialog_delete_conversation_warning">Eliminar esta conversa\?</string> + <string name="action_delete_conversation">Eliminar conversa</string> + <string name="action_unbookmark">Eliminar marcador</string> + <string name="pref_title_confirm_favourites">Pedir confirmación antes de favorecer</string> + <string name="duration_14_days">14 días</string> + <string name="duration_30_days">30 días</string> + <string name="duration_60_days">60 días</string> + <string name="duration_90_days">90 días</string> + <string name="duration_180_days">180 días</string> + <string name="duration_365_days">365 días</string> + <string name="tusky_compose_post_quicksetting_label">Redactar publicación</string> + <string name="notification_sign_up_format">%1$s rexistrouse</string> + <string name="pref_title_notification_filter_sign_ups">hai unha nova usuaria</string> + <string name="notification_sign_up_name">Rexistros</string> + <string name="notification_sign_up_description">Notificacións sobre novas usuarias</string> + <string name="pref_title_notification_filter_updates">Foi editada unha publicación coa que interactuei</string> + <string name="notification_update_name">Edicións da publicación</string> + <string name="account_date_joined">Creada %1$s</string> + <string name="tips_push_notification_migration">Volve a acceder con tódalas contas para activar as notificacións push.</string> + <string name="title_login">Acceder</string> + <string name="notification_update_description">Notificacións cando son editadas publicacións coas que interactuaches</string> + <string name="dialog_push_notification_migration">Para poder usar as notificacións push vía UnifiedPush, Tusky require o permiso para subscribirse ás notificacións do teu servidor Mastodon. É necesario volver a acceder para cambiar os ámbitos OAuth concedidos a Tusky. Usando aquí, ou nas preferencias da Conta, a opción de volver a acceder conservarás os borradores locais e caché.</string> + <string name="dialog_push_notification_migration_other_accounts">Volveches a acceder para obter as notificacións push en Tusky. Aínda así tes algunha outra conta que non foi migrada a este modo. Cambia a esas contas e volve a conectar unha a unha para activar o soporte para notificacións de UnifiedPush.</string> + <string name="title_migration_relogin">Volve a acceder para ter notificacións push</string> + <string name="notification_update_format">%1$s editou a publicación</string> + <string name="action_dismiss">Desbotar</string> + <string name="action_details">Detalles</string> + <string name="error_could_not_load_login_page">Non se puido cargar a páxina de inicio.</string> + <string name="saving_draft">Gardando borrador…</string> + <string name="status_count_one_plus">1+</string> + <string name="action_edit_image">Editar imaxe</string> + <string name="error_loading_account_details">Fallou a carga dos detalles da conta</string> + <string name="error_image_edit_failed">A imaxe non puido ser editada.</string> + <string name="set_focus_description">Toca ou arrastra o círculo para elexir onde centrar a imaxe e sexa máis visible nas miniaturas.</string> + <string name="duration_no_change">(Sen cambio)</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="error_multimedia_size_limit">Os ficheiros de vídeo e audio non poden superar os %1$s MB.</string> + <string name="description_post_language">Idioma de publicación</string> + <string name="action_set_focus">Establece foco</string> + <string name="error_following_hashtag_format">Erro ao seguir #%1$s</string> + <string name="error_unfollowing_hashtag_format">Error ao retirar seguimento de #%1$s</string> + <string name="instance_rule_title">Normas de %1$s</string> + <string name="instance_rule_info">Ao iniciar sesión aceptas as normas de %1$s.</string> + <string name="action_add_reaction">engadir reacción</string> + <string name="compose_save_draft_loses_media">Gardar borrador\? (Os adxuntos serán subidos outra vez cando restablezas o borrador.)</string> + <string name="pref_title_show_self_username">Mostrar identificador na barra ferramentas</string> + <string name="delete_scheduled_post_warning">Eliminar publicación programada\?</string> + <string name="failed_to_pin">Fallo ao Fixar</string> + <string name="failed_to_unpin">Fallo ao Desafixar</string> + <string name="pref_show_self_username_always">Sempre</string> + <string name="pref_show_self_username_disambiguate">Cando hai máis dunha conta activa</string> + <string name="pref_show_self_username_never">Nunca</string> + <string name="notification_report_description">Notificacións sobre temas de moderación</string> + <string name="report_category_spam">Spam</string> + <string name="action_add_or_remove_from_list">Engadir ou quitar da lista</string> + <string name="failed_to_add_to_list">Fallou a adición da conta á lista</string> + <string name="failed_to_remove_from_list">Fallou a eliminación da conta da lista</string> + <string name="action_unfollow_hashtag_format">Non seguir #%1$s\?</string> + <string name="pref_default_post_language">Idioma de publicación por defecto</string> + <string name="notification_report_name">Denuncias</string> + <string name="no_lists">Non tes listas.</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="report_category_violation">Faltar a unha norma</string> + <string name="report_category_other">Outra</string> + <string name="error_following_hashtags_unsupported">Esta instancia non ten soporte para seguimento de cancelos.</string> + <string name="title_followed_hashtags">Cancelos seguidos</string> + <string name="status_created_at_now">agora</string> + <string name="notification_report_format">Nova denuncia en %1$s</string> + <string name="notification_header_report_format">%1$s denunciou a %2$s</string> + <string name="notification_summary_report_format">%1$s · %2$d publicacións fixadas</string> + <string name="confirmation_hashtag_unfollowed">Xa non segues a #%1$s</string> + <string name="pref_title_notification_filter_reports">hai unha nova denuncia</string> + <string name="hint_media_description_missing">O multimedia debería ter unha descrición.</string> + <string name="pref_title_http_proxy_port_message">O porto debe estar entre %1$d e %2$d</string> + <string name="error_muting_hashtag_format">Erro ao acalar #%1$s</string> + <string name="error_unmuting_hashtag_format">Erro ao reactivar #%1$s</string> + <string name="description_post_edited">Editado</string> + <string name="post_media_alt">ALT</string> + <string name="post_edited">Editado %1$s</string> + <string name="error_status_source_load">Fallou a carga da fonte do estado desde o servidor.</string> + <string name="a11y_label_loading_thread">Cargando fío</string> + <string name="mute_notifications_switch">Acalar notificacións</string> + <string name="title_edits">Edicións</string> + <string name="status_edit_info">Editada: %1$s</string> + <string name="status_created_info">Creado por %1$s</string> + <string name="action_discard">Desbotar cambios</string> + <string name="action_continue_edit">Continuar a edición</string> + <string name="compose_unsaved_changes">Hai cambios non gardados.</string> + <string name="action_share_account_link">Comparte ligazón da conta</string> + <string name="action_share_account_username">Comparte identificador da conta</string> + <string name="send_account_link_to">Compartir URL da conta en…</string> + <string name="send_account_username_to">Compartir identificador da conta en…</string> + <string name="account_username_copied">Identificador copiado</string> + <string name="pref_title_reading_order">Orde de lectura</string> + <string name="pref_reading_order_oldest_first">Antigo primeiro</string> + <string name="pref_reading_order_newest_first">Novidades primeiro</string> + <string name="pref_summary_http_proxy_missing"><non establecido></string> + <string name="pref_summary_http_proxy_disabled">Desactivado</string> + <string name="pref_summary_http_proxy_invalid"><non válido></string> + <string name="action_post_failed">Fallou a subida</string> + <string name="action_post_failed_detail">Fallou a subida da publicación e gardouse non borradores. +\n +\nPode que o servidor non estive accesible ou que rexeitase a publicación.</string> + <string name="action_post_failed_detail_plural">Fallou a subida das túas publicacións e gardáronse nos borradores. +\n +\nPode que o servidor non estivese accesible ou que rexeitase as publicacións.</string> + <string name="action_post_failed_show_drafts">Mostrar borradores</string> + <string name="action_post_failed_do_nothing">Desbotar</string> + <string name="action_browser_login">Acceder no Navegador</string> + <string name="description_browser_login">Pode ter soporte para métodos adicionais de autenticación, pero require un navegador soportado.</string> + <string name="description_login">Funciona case sempre. Non se filtran datos a outras apps.</string> + <string name="accessibility_talking_about_tag">%1$d persoas están falando acerca do cancelo %2$s</string> + <string name="title_public_trending_hashtags">Cancelos en voga</string> + <string name="total_usage">Uso total</string> + <string name="total_accounts">Total de contas</string> + <string name="dialog_follow_hashtag_title">Seguir cancelo</string> + <string name="dialog_follow_hashtag_hint">#cancelo</string> + <string name="help_empty_home">Esta é a túa <b>cronoloxía de inicio</b>. Mostra as publicacións recentes das contas que segues. +\n +\nPara atopar contas podes mirar nalgunha das outras cronoloxías. Por exemplo, na cronoloxía local da túa instancia [iconics gmd_group]. Ou buscalas polo seu nome [iconics gmd_search]; por exemplo busca Tusky para atopar a nosa conta en Mastodon.</string> + <string name="post_media_image">Imaxe</string> + <string name="hint_filter_title">O meu filtro</string> + <string name="action_refresh">Actualizar</string> + <string name="notification_unknown_name">Descoñecido</string> + <string name="socket_timeout_exception">Estamos tardando demasiado en conectar co servidor</string> + <string name="ui_error_unknown">razón descoñecida</string> + <string name="ui_error_bookmark">Fallou engadir a marcadores: %1$s</string> + <string name="ui_error_clear_notifications">Fallou a limpeza das notificacións: %1$s</string> + <string name="ui_error_vote">Fallou a votación na enquisa: %1$s</string> + <string name="ui_error_favourite">Fallou favorecer a publicación: %1$s</string> + <string name="ui_error_reblog">Fallou promover a publicación: %1$s</string> + <string name="ui_error_accept_follow_request">Fallou a aceptación da solicitude de seguimento: %1$s</string> + <string name="ui_error_reject_follow_request">Fallou o rexeitamento da solicitude de seguimento: %1$s</string> + <string name="ui_success_accepted_follow_request">Aceptado o seguimento</string> + <string name="ui_success_rejected_follow_request">Bloqueada a solicitude de seguimento</string> + <string name="status_filtered_show_anyway">Mostrar igualmente</string> + <string name="status_filter_placeholder_label_format">Filtrado: %1$s</string> + <string name="pref_title_account_filter_keywords">Perfís</string> + <string name="label_filter_title">Título</string> + <string name="filter_action_warn">Aviso</string> + <string name="filter_action_hide">Agochar</string> + <string name="filter_description_warn">Agochar cun aviso</string> + <string name="filter_description_hide">Agochar completamente</string> + <string name="label_filter_action">Acción do filtro</string> + <string name="label_filter_context">Ámbitos para o filtro</string> + <string name="label_filter_keywords">Palabras chave a filtrar</string> + <string name="action_add">Engadir</string> + <string name="filter_keyword_display_format">%1$s (palabra completa)</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="pref_title_show_stat_inline">Mostrar estatísticas da publicación na cronoloxía</string> + <string name="filter_keyword_addition_title">Engadir palabra</string> + <string name="filter_edit_keyword_title">Editar palabra</string> + <string name="select_list_manage">Xestionar listas</string> + <string name="error_missing_edits">O teu servidor sabe que a publicación foi editada, pero non ten unha copia das edición, polo que non pode mostrarchas. +\n +\nÉ un <a href="https://github.com/mastodon/mastodon/issues/25398">problema coñecido</a> en Mastodon.</string> + <string name="load_newest_notifications">Cargar as notificacións máis recentes</string> + <string name="compose_delete_draft">Eliminar borrador\?</string> + <string name="pref_ui_text_size">Tamaño da letra</string> + <string name="notification_listenable_worker_name">Actividade en segundo plano</string> + <string name="notification_listenable_worker_description">Notificacións cando Tusky está a funcionar en segundo plano</string> + <string name="notification_notification_worker">Obtendo as notificacións…</string> + <string name="notification_prune_cache">Mantemento da caché…</string> + <string name="error_media_upload_sending_fmt">Fallou a subida: %1$s</string> + <string name="about_device_info_title">O teu dispositivo</string> + <string name="about_device_info">%1$s %2$s +\nVersión de Android: %3$s +\nVersión SDK: %4$d</string> + <string name="about_account_info_title">A túa conta</string> + <string name="about_account_info">\@%1$s@%2$s +\nVersión: %3$s</string> + <string name="about_copied">Copiouse a información sobre o dispositivo e versión</string> + <string name="about_copy">Copiar a información da versión e o dispositivo</string> + <string name="dialog_delete_filter_positive_action">Eliminar</string> + <string name="list_exclusive_label">Agochar na cronoloxía de inicio</string> + <string name="dialog_delete_filter_text">Eliminar o filtro \'%1$s\'\?</string> + <string name="error_media_playback">Fallou a reprodución: %1$s</string> + <string name="help_empty_conversations">Aquí están as túas <b>mensaxes privadas</b>; tamén chamadas conversas ou mensaxes directas (DM); +\n +\nAs mensaxes privadas créanse establecendo a visibilidade [iconics gmd_public] da publicación como [iconics gmd_mail] <i>Directa</i> e mencionando a unha ou máis usuarias no texto. +\n +\nPor exemplo podes ir ao perfil dunha conta e tocar no botón escribir [iconics gmd_edit] e cambiar a visibilidade. </string> + <string name="muting_hashtag_success_format">Acalar o cancelo #%1$s cun aviso</string> + <string name="unmuting_hashtag_success_format">Reactivar o cancelo #%1$s</string> + <string name="action_view_filter">Ver filtro</string> + <string name="following_hashtag_success_format">Agora segues o cancelo #%1$s</string> + <string name="unfollowing_hashtag_success_format">Xa non segues o cancelo #%1$s</string> + <string name="dialog_save_profile_changes_message">Queres gardar os cambios no teu perfil\?</string> + <string name="help_empty_lists">Esta é a <b>vista de listas</b>. Podes definir varias listas privadas e engadir contas a elas. +\n +\nTEN EN CONTA que só podes engadir contas que estás a seguir. +\n +\nEstas listas pódense usar como pestanas configurando as Pestanas nas Preferencias da Conta [iconics gmd_account_circle] [iconics gmd_navigate_next]. </string> + <string name="error_blocking_domain">Non se acalou a %1$s: %2$s</string> + <string name="error_unblocking_domain">Non se reactivou a %1$s: %2$s</string> + <string name="label_image">Imaxe</string> + <string name="app_theme_system_black">Usar o deseño do Sistema (negro)</string> + <string name="title_public_trending_statuses">Publicacións en voga</string> + <string name="list_reply_policy_none">Ninguén</string> + <string name="list_reply_policy_list">Compoñentes da lista</string> + <string name="list_reply_policy_followed">Calquera usuaria seguida</string> + <string name="list_reply_policy_label">Mostrar respostas a</string> + <string name="pref_title_show_self_boosts_description">Alguén que promove as súas propias publicacións</string> + <string name="pref_title_show_self_boosts">Mostrar auto-promocións</string> + <string name="pref_title_per_timeline_preferences">Preferencias nas cronoloxías</string> + <string name="pref_title_show_notifications_filter">Mostrar Filtro das notificacións</string> + <string name="reply_sending">A enviar…</string> + <string name="reply_sending_long">Enviouse a resposta.</string> + <string name="action_translate">Traducir</string> + <string name="action_show_original">Mostrar orixinal</string> + <string name="label_translated">Traducida do %1$s con %2$s</string> + <string name="label_translating">A traducir…</string> + <string name="ui_error_translate">Non se puido traducir: %1$s</string> + <string name="dialog_follow_warning">Seguir esta conta?</string> + <string name="report_category_legal">Legal</string> + <string name="pref_title_confirm_follows">Pedir confirmación antes de seguir</string> + <string name="unknown_notification_type">Tipo de notificación descoñecido</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml new file mode 100644 index 0000000..add1176 --- /dev/null +++ b/app/src/main/res/values-hi/strings.xml @@ -0,0 +1,401 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="action_login">हिंदी</string> + <string name="title_favourites">पसंदीदा</string> + <string name="title_drafts">प्रारूप</string> + <string name="action_logout">लॉग आउट</string> + <string name="action_view_preferences">प्राथमिकताएं</string> + <string name="action_view_account_preferences">खाता प्राथमिकताएं</string> + <string name="action_edit_profile">प्रोफाइल एडिट करें</string> + <string name="action_search">खोज</string> + <string name="about_title_activity">के बारे में</string> + <string name="title_posts_with_replies">उत्तरों के साथ</string> + <string name="title_followers">अनुगामी</string> + <string name="action_reply">उत्तर दें</string> + <string name="action_follow">फ़ॉलो</string> + <string name="action_unfollow">अनफ़ॉलो</string> + <string name="action_block">ब्लॉक</string> + <string name="action_unblock">अनब्लॉक</string> + <string name="action_hide_reblogs">बूस्ट छिपाएं</string> + <string name="action_show_reblogs">बूस्ट दिखाएं</string> + <string name="action_report">रिपोर्ट करें</string> + <string name="action_edit">एडिट करें</string> + <string name="action_delete">डिलीट करें</string> + <string name="action_delete_and_redraft">डिलीट एवं रिड्राफ्ट करें</string> + <string name="action_send">टूट करें</string> + <string name="action_send_public">टूट!</string> + <string name="action_retry">पुनः प्रयास करें</string> + <string name="action_close">बंद करें</string> + <string name="action_view_profile">प्रोफाइल</string> + <string name="description_visibility_private">अनुगामी</string> + <string name="error_media_upload_sending">अपलोड विफल रहा।</string> + <string name="error_media_download_permission">मीडिया को स्टोर करने की अनुमति आवश्यक है।</string> + <string name="error_media_upload_permission">मीडिया पढ़ने की अनुमति आवश्यक है।</string> + <string name="error_media_upload_opening">वह फ़ाइल नहीं खोली जा सकी।</string> + <string name="error_media_upload_type">उस प्रकार की फ़ाइल अपलोड नहीं की जा सकती।</string> + <string name="error_generic">एक त्रुटि हुई।</string> + <string name="action_reset_schedule">रीसेट</string> + <string name="error_retrieving_oauth_token">लॉगिन टोकन प्राप्त करने में विफल।</string> + <string name="error_authorization_denied">प्राधिकरण करने के से इनकार कर दिया।</string> + <string name="error_authorization_unknown">एक अज्ञात प्राधिकरण त्रुटि हुई।</string> + <string name="error_no_web_browser_found">उपयोग करने के लिए वेब ब्राउज़र नहीं मिला।</string> + <string name="error_invalid_domain">अमान्य डोमेन दर्ज किया गया</string> + <string name="error_empty">यह खाली नहीं हो सकता।</string> + <string name="error_network">नेटवर्क त्रुटि हुई! कृपया अपना कनेक्शन जांचें और पुनः प्रयास करें!</string> + <string name="title_lists">सूचियाँ</string> + <string name="action_lists">सूचियाँ</string> + <string name="description_poll">जनमत के विकल्प: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="action_view_bookmarks">बुकमार्क</string> + <string name="edit_poll">संपादित करें</string> + <string name="pref_title_notification_filter_poll">जनमत खत्म हो गया हैं</string> + <string name="action_add_poll">जनमत शुरू करें</string> + <string name="create_poll_title">पोल</string> + <string name="title_accounts">खाता</string> + <string name="button_done">पूर्ण</string> + <string name="button_back">पीछे</string> + <string name="button_continue">जारी रखें</string> + <string name="poll_vote">वोट</string> + <string name="poll_info_closed">बन्द है</string> + <string name="compose_shortcut_short_label">लिखें</string> + <string name="title_posts_pinned">पिन की गई</string> + <string name="title_posts">पोस्ट</string> + <string name="title_home">घर</string> + <string name="license_description">टस्की में निम्नलिखित ओपन सोर्स परियोजनाओं से कोड और संपत्ति हैं:</string> + <string name="restart_emoji">इन परिवर्तनों को लागू करने के लिए आपको टस्की को पुनः आरंभ करना होगा</string> + <string name="about_tusky_account">टस्की की प्रोफाइल</string> + <string name="about_bug_feature_request_site">बग रिपोर्ट और सुविधा अनुरोध: +\n https://github.com/tuskyapp/Tusky/issues</string> + <string name="about_project_site">परियोजना की वेबसाइट: +\n https://tusky.app</string> + <string name="about_tusky_license">टस्की स्वतंत्र और ओपन-सोर्स सॉफ्टवेयर है। यह GNU जनरल पब्लिक लाइसेंस संस्करण 3 के तहत लाइसेंस प्राप्त है। आप लाइसेंस यहां देख सकते हैं: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="about_powered_by_tusky">टस्की द्वारा संचालित</string> + <string name="about_tusky_version">टस्की %1$s</string> + <string name="description_account_locked">बंद खाता</string> + <string name="notification_summary_small">%1$s तथा %2$s</string> + <string name="notification_summary_medium">%1$s, %2$s, तथा %3$s</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s तथा %4$d अन्य लोग</string> + <string name="notification_poll_name">जनमत</string> + <string name="notification_favourite_name">पसंदीदा</string> + <string name="post_text_size_largest">सबसे बड़ा</string> + <string name="post_text_size_large">बड़ा</string> + <string name="post_text_size_medium">मध्यम</string> + <string name="post_text_size_small">छोटा</string> + <string name="post_text_size_smallest">सबसे छोटा</string> + <string name="post_privacy_unlisted">असूचीबद्ध</string> + <string name="post_privacy_public">सार्वजनिक</string> + <string name="pref_main_nav_position_option_bottom">सबसे नीचे</string> + <string name="pref_main_nav_position_option_top">ऊपर</string> + <string name="pref_main_nav_position">मुख्य नेविगेशन स्थिति</string> + <string name="pref_failed_to_sync">सेटिंग्स सिंक करने में विफल</string> + <string name="pref_default_media_sensitivity">हमेशा मीडिया को संवेदनशील के रूप में चिह्नित करें</string> + <string name="pref_default_post_privacy">डिफ़ॉल्ट पोस्ट गोपनीयता</string> + <string name="pref_title_show_media_preview">मीडिया पूर्वावलोकन डाउनलोड करें</string> + <string name="pref_title_show_replies">जवाब दिखाएँ</string> + <string name="pref_title_post_tabs">टैब्स</string> + <string name="pref_title_gradient_for_media">छिपे हुए मीडिया के लिए रंगीन ग्रेडिएंट दिखाएं</string> + <string name="pref_title_bot_overlay">बॉट्स के लिए संकेतक दिखाएं</string> + <string name="pref_title_language">भाषा</string> + <string name="pref_title_custom_tabs">क्रोम कस्टम टैब का उपयोग करें</string> + <string name="pref_title_browser_settings">ब्राउज़र</string> + <string name="app_theme_system">सिस्टम डिज़ाइन का उपयोग करें</string> + <string name="app_theme_auto">सूर्यास्त के समय स्वचालित</string> + <string name="app_theme_black">Black</string> + <string name="app_them_dark">अंधकार</string> + <string name="pref_title_timeline_filters">फिल्टर</string> + <string name="pref_title_appearance_settings">दिखावट</string> + <string name="pref_title_notification_filter_mentions">उल्लेख किया</string> + <string name="pref_title_notification_filters">मुझे सूचित करें जब</string> + <string name="pref_title_notification_alert_light">रोशनी के साथ सूचित करें</string> + <string name="pref_title_notification_alert_vibrate">कंपन के साथ सूचित करें</string> + <string name="pref_title_notification_alert_sound">एक ध्वनि के साथ सूचित करें</string> + <string name="pref_title_notifications_enabled">सूचनाएं</string> + <string name="pref_title_edit_notification_settings">सूचनाएं</string> + <string name="visibility_direct">प्रत्यक्ष: केवल उल्लिखित उपयोगकर्ताओं को पोस्ट करें</string> + <string name="visibility_private">केवल फ़ॉलोअर के लिए : केवल फ़ॉलोअर के लिए पोस्ट करें</string> + <string name="visibility_unlisted">असूचीबद्ध: सार्वजनिक टाइमलाइन में न दिखाएं</string> + <string name="visibility_public">सार्वजनिक: सार्वजनिक टाइमलाइन पर पोस्ट करें</string> + <string name="dialog_mute_warning">म्यूट @%1$s\?</string> + <string name="mute_domain_warning_dialog_ok">संपूर्ण डोमेन छिपाएं</string> + <string name="dialog_download_image">डाउनलोड</string> + <string name="dialog_message_uploading_media">अपलोड हो रहा है…</string> + <string name="login_connection">कनेक्ट कर रहे…</string> + <string name="label_header">हैडर</string> + <string name="label_avatar">अवतार</string> + <string name="label_quick_reply">जवाब दें…</string> + <string name="search_no_results">कोई परिणाम नहीं</string> + <string name="hint_search">खोज</string> + <string name="hint_note">जीवनी</string> + <string name="hint_display_name">प्रदर्शित होने वाला नाम</string> + <string name="hint_content_warning">विषय वस्तु चेतावनी</string> + <string name="hint_compose">क्या हो रहा है\?</string> + <string name="confirmation_reported">भेज दिया!</string> + <string name="downloading_media">मीडिया डाउनलोड हो रहा है</string> + <string name="download_media">मीडिया डाउनलोड करें</string> + <string name="title_links_dialog">लिंक</string> + <string name="action_copy_link">लिंक कॉपी करें</string> + <string name="action_open_media_n">मीडिया खोलें #%1$d</string> + <string name="title_mentions_dialog">ज़िक्र</string> + <string name="title_hashtags_dialog">हैशटैग</string> + <string name="action_open_faved_by">पसंदीदा दिखाएँ</string> + <string name="action_hashtags">हैशटैग</string> + <string name="action_mentions">ज़िक्र</string> + <string name="action_add_tab">ऐड टैब</string> + <string name="action_schedule_post">अनुसूची टूट</string> + <string name="action_emoji_keyboard">इमोजी कीबोर्ड</string> + <string name="action_content_warning">विषय वस्तु चेतावनी</string> + <string name="action_toggle_visibility">टूट दृश्यता</string> + <string name="action_access_scheduled_posts">अनुसूचित टूट</string> + <string name="action_accept">स्वीकार करें</string> + <string name="action_access_drafts">ड्राफ्ट</string> + <string name="action_reject">अस्वीकार करें</string> + <string name="action_undo">पूर्ववत करें</string> + <string name="action_edit_own_profile">संपादित करें</string> + <string name="action_save">सेव</string> + <string name="action_hide_media">मीडिया छिपाओ</string> + <string name="action_mention">ज़िक्र</string> + <string name="action_unmute_conversation">बातचीत को अनम्यूट करें</string> + <string name="action_unmute_domain">अनम्यूट %1$s</string> + <string name="action_unmute">अनम्यूट</string> + <string name="action_mute">म्यूट</string> + <string name="action_mute_domain">म्यूट %1$s</string> + <string name="action_mute_conversation">बातचीत को म्यूट करें</string> + <string name="action_share">शेयर</string> + <string name="action_photo_take">फोटो खींचिए</string> + <string name="action_add_media">ऐड मीडिया</string> + <string name="action_open_in_web">ब्राउज़र में खोलें</string> + <string name="action_view_media">मीडिया</string> + <string name="error_sender_account_gone">टोट भेजने में त्रुटि।</string> + <string name="action_bookmark">बुकमार्क</string> + <string name="action_reblog">बढ़ावा दें</string> + <string name="action_quick_reply">तुरंत जवाब</string> + <string name="report_comment_hint">अतिरिक्त टिप्पणियां\?</string> + <string name="report_username_format">रिपोर्ट @%1$s</string> + <string name="notification_favourite_format">%1$s ने आपके टूट को पसंद किया</string> + <string name="notification_reblog_format">%1$s ने आपके टूट को बढावा दिया</string> + <string name="footer_empty">यहाँ कुछ नहीं। ताज़ा करने के लिए नीचे खींचो!</string> + <string name="message_empty">यहाँ कुछ नहीं।</string> + <string name="post_content_warning_show_less">कम दिखाएं</string> + <string name="post_content_warning_show_more">और दिखाओ</string> + <string name="post_sensitive_media_directions">देखने के लिए क्लिक करें</string> + <string name="post_media_hidden_title">मीडिया छिपा हुआ</string> + <string name="post_sensitive_media_title">संवेदनशील विषय वस्तु</string> + <string name="post_username_format">\@%1$s</string> + <string name="title_licenses">लाइसेंस</string> + <string name="title_view_thread">टूट</string> + <string name="title_scheduled_posts">अनुसूचित टूट</string> + <string name="title_domain_mutes">छिपे हुए डोमेन</string> + <string name="title_mutes">म्यूट किए गए उपयोगकर्ता</string> + <string name="title_bookmarks">बुकमार्क</string> + <string name="title_tab_preferences">टैब्स</string> + <string name="title_direct_messages">सीधे संदेश</string> + <string name="title_public_federated">संघीय</string> + <string name="title_public_local">स्थानीय</string> + <string name="title_notifications">सूचनाएं</string> + <string name="add_poll_choice">विकल्प जोड़ें</string> + <string name="poll_new_choice_hint">विकल्प %1$d</string> + <string name="poll_allow_multiple_choices">कई विकल्प</string> + <string name="duration_7_days">7 दिन</string> + <string name="duration_3_days">3 दिन</string> + <string name="duration_1_day">1 दिन</string> + <string name="duration_6_hours">6 घंटे</string> + <string name="duration_1_hour">1 घंटा</string> + <string name="duration_30_min">30 मिनिट</string> + <string name="duration_5_min">5 मिनट</string> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d घंटा शेष</item> + <item quantity="other">%1$d घंटे शेष</item> + </plurals> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d दिन शेष</item> + <item quantity="other">%1$d दिन शेष</item> + </plurals> + <string name="poll_ended_created">आपके द्वारा बनाया गया एक जनमत समाप्त हो गया है</string> + <string name="poll_ended_voted">आपके द्वारा मतदान किया गया जनमत समाप्त हो गया है</string> + <string name="poll_info_time_absolute">%1$s समाप्त होगा</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s व्यक्ति</item> + <item quantity="other">%1$s लोग</item> + </plurals> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s वोट</item> + <item quantity="other">%1$s वोट</item> + </plurals> + <string name="hashtags">हैशटैग</string> + <string name="poll_info_format"> <!-- 15 वोट • 1 घंटा बाकी --> %1$s • %2$s</string> + <string name="list">सूची</string> + <string name="description_post_bookmarked">बुकमार्क किया हुआ</string> + <string name="hint_list_name">सूची का नाम</string> + <string name="description_post_media_no_description_placeholder">कोई विवरण नहीं</string> + <string name="description_post_media">मीडिया: %1$s</string> + <string name="profile_metadata_add">जानकारी जोड़ें</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="profile_metadata_label">प्रोफ़ाइल मेटाडेटा</string> + <string name="download_failed">डाउनलोड विफल</string> + <string name="action_open_post">टूट खोलें</string> + <string name="performing_lookup_title">देखने की क्रिया जारी…</string> + <string name="system_default">सिस्टम डिफ़ॉल्ट</string> + <string name="emoji_style">इमोजी का अंदाज</string> + <string name="send_post_notification_cancel_title">भेजना रद्द हो गया</string> + <string name="send_post_notification_channel_name">टूट भेज रहे</string> + <string name="send_post_notification_title">टूट भेज रहे…</string> + <string name="compose_save_draft">लिखने को सुरक्षित करें\?</string> + <string name="lock_account_label">खाता लॉक करें</string> + <string name="action_set_caption">कैप्शन सेट करें</string> + <string name="add_account_name">खाता जोड़ो</string> + <string name="filter_dialog_whole_word">पूरा शब्द</string> + <string name="filter_edit_title">फ़िल्टर संपादित करें</string> + <string name="filter_addition_title">फिल्टर लगाएं</string> + <string name="pref_title_public_filter_keywords">सार्वजनिक टाइमलाइन</string> + <string name="load_more_placeholder_text">और लोड करें</string> + <string name="pref_title_show_cards_in_timelines">टाइमलाइन में लिंक प्रीव्यू दिखाएं</string> + <string name="warning_scheduling_interval">मास्टोडन का न्यूनतम शेड्यूलिंग अंतराल 5 मिनट है।</string> + <string name="no_drafts">आपके पास कोई ड्राफ्ट नहीं है।</string> + <string name="post_lookup_error_format">%1$s पोस्ट खोजने में त्रुटि</string> + <string name="pref_title_enable_swipe_for_tabs">टैब के बीच स्विच करने के लिए स्वाइप जेस्चर को सक्षम करें</string> + <string name="failed_search">खोज करने में विफल</string> + <string name="report_description_remote_instance">खाता किसी अन्य सर्वर से है। रिपोर्ट की अनाम प्रति वहां भी भेजें\?</string> + <string name="report_description_1">रिपोर्ट आपके सर्वर मॉडरेटर को भेजी जाएगी। आप इस बारे में स्पष्टीकरण दे सकते हैं कि आप इस खाते को रिपोर्ट क्यों कर रहे हैं:</string> + <string name="failed_report">रिपोर्ट करने में विफल</string> + <string name="report_remote_instance">%1$s को आगे भेजें</string> + <string name="hint_additional_info">अतिरिक्त टिप्पणियां</string> + <string name="report_sent_success">\@%1$s सफलतापूर्वक रिपोर्ट किए गए</string> + <string name="compose_preview_image_description">तस्वीरों के लिए क्रिया %1$s</string> + <string name="notification_clear_text">क्या आप अपनी सभी सूचनाओं को स्थायी रूप से साफ़ करना चाहते हैं\?</string> + <string name="compose_shortcut_long_label">टूट लिखें</string> + <string name="filter_apply">लागू करें</string> + <string name="notifications_apply_filter">फ़िल्टर</string> + <string name="notifications_clear">साफ करें</string> + <string name="select_list_title">सूची का चयन करें</string> + <string name="add_hashtag_title">हैशटैग जोड़ें</string> + <string name="description_visibility_direct">प्रत्यक्ष</string> + <string name="description_visibility_unlisted">असूचीबद्ध</string> + <string name="description_visibility_public">सार्वजनिक</string> + <string name="description_post_cw">विषय वस्तु चेतावनी: %1$s</string> + <string name="conversation_more_recipients">%1$s, %2$s तथा %3$d अन्य लोग</string> + <string name="conversation_2_recipients">%1$s तथा %2$s</string> + <string name="conversation_1_recipients">%1$s</string> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> ने बढ़ावा दिया</item> + <item quantity="other"><b>%1$s</b> ने बढ़ावा दिया</item> + </plurals> + <string name="pin_action">पिन</string> + <string name="unpin_action">अनपिन</string> + <string name="label_remote_account">नीचे दी गई जानकारी उपयोगकर्ता की प्रोफ़ाइल को अपूर्ण रूप से दर्शा सकती है। ब्राउज़र में पूर्ण प्रोफ़ाइल खोलने के लिए यहां दबाएं।</string> + <string name="pref_title_absolute_time">पूर्ण समय का उपयोग करें</string> + <string name="profile_metadata_content_label">विषय वस्तु</string> + <string name="profile_metadata_label_label">लेबल</string> + <string name="license_apache_2">अपाचे लाइसेंस के तहत लाइसेंस प्राप्त (प्रतिलिपि नीचे उपलब्ध)</string> + <string name="account_moved_description">%1$s यहां चले गए हैं:</string> + <string name="profile_badge_bot_text">बोट</string> + <string name="caption_notoemoji">गूगल का इमोजी सेट</string> + <string name="caption_twemoji">मास्टोडन का मानक इमोजी सेट</string> + <string name="caption_systememoji">आपके डिवाइस का डिफ़ॉल्ट इमोजी सेट</string> + <string name="restart">पुनः आरंभ करें</string> + <string name="later">बाद में</string> + <string name="restart_required">एप्लिकेशन को पुनः आरंभ की आवश्यकता है</string> + <string name="download_fonts">आपको पहले इस इमोजी सेट को डाउनलोड करना होगा</string> + <string name="action_compose_shortcut">लिखें</string> + <string name="send_post_notification_saved_content">टूट की एक प्रति आपके ड्राफ्ट में सहेज ली गई है</string> + <string name="send_post_notification_error_title">टूट भेजने में त्रुटि</string> + <string name="filter_dialog_remove_button">हटाएँ</string> + <string name="action_remove">हटा दें</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">दृष्टिहीन लोगों के लिए वर्णन करें +\n(%1$d वर्ण सीमा)</item> + </plurals> + <string name="compose_active_account_description">%1$s खाते के साथ पोस्ट कर रहे</string> + <string name="action_remove_from_list">सूची से खाता निकालें</string> + <string name="action_add_to_list">सूची में खाता जोड़ें</string> + <string name="hint_search_people_list">उन लोगों की खोज करें जिन्हें आप फ़ॉलो करते हैं</string> + <string name="action_delete_list">सूची हटाएँ</string> + <string name="action_rename_list">सूची का नाम बदलें</string> + <string name="action_create_list">एक सूची बनाएं</string> + <string name="error_delete_list">सूची नहीं हटाई जा सकी</string> + <string name="error_rename_list">सूची का नाम नहीं बदल सके</string> + <string name="error_create_list">सूची नहीं बना सके</string> + <string name="add_account_description">नया मास्टोडन खाता जोड़ें</string> + <string name="filter_add_description">फ़िल्टर करने के लिए वाक्यांश</string> + <string name="filter_dialog_whole_word_description">जब संकेत शब्द या वाक्यांश केवल अल्फ़ान्यूमेरिक होता है, तो यह केवल तभी लागू होगा जब यह पूरे शब्द से मेल खाता होगा</string> + <string name="filter_dialog_update_button">अपडेट करें</string> + <string name="pref_title_thread_filter_keywords">संवाद</string> + <string name="replying_to">\@%1$s को जवाब दे रहे है</string> + <string name="title_media">मीडिया</string> + <string name="pref_title_alway_show_sensitive_media">संवेदनशील विषय वस्तु</string> + <string name="follows_you">आपको फॉलो करते है</string> + <string name="pref_title_notification_filter_follow_requests">फ़ॉलो करने का अनुरोध किया</string> + <string name="state_follow_requested">फ़ॉलो करने का अनुरोध किया हुआ</string> + <string name="post_media_video">वीडियो</string> + <string name="post_media_images">तस्वीरें</string> + <string name="post_share_link">टूट का लिंक साझा करें</string> + <string name="post_share_content">टूट की विषय वस्तु साझा करें</string> + <string name="notification_mention_format">%1$s ने आपका जिक्र किया</string> + <string name="notification_poll_description">जनमत खत्म होने के बारे में सूचनाएं</string> + <string name="notification_favourite_description">सूचनाएं जब आपके टूट पसंद किये जाये</string> + <string name="notification_follow_request_description">फ़ॉलो अनुरोध के बारे में सूचनाएं</string> + <string name="notification_follow_request_name">फ़ॉलो अनुरोध</string> + <string name="notification_mention_descriptions">नए ज़िक्र के बारे में सूचनाएं</string> + <string name="notification_mention_name">नए ज़िक्र</string> + <string name="pref_publishing">प्रकाशित कर रहे हैं (सर्वे के साथ समन्वयित)</string> + <string name="pref_title_http_proxy_port">एच टी टी पी प्रॉक्सी पोर्ट</string> + <string name="pref_title_http_proxy_server">एच टी टी पी प्रॉक्सी सर्वर</string> + <string name="pref_title_http_proxy_enable">एच टी टी पी प्रॉक्सी सक्षम करें</string> + <string name="pref_title_http_proxy_settings">एच टी टी पी प्रॉक्सी</string> + <string name="pref_title_proxy_settings">प्रॉक्सी</string> + <string name="pref_title_post_filter">टाइमलाइन छानने का काम</string> + <string name="app_theme_light">प्रकाश</string> + <string name="pref_title_timelines">टाइमलाइन</string> + <string name="pref_title_app_theme">एप्लिकेशन थीम</string> + <string name="pref_title_notification_filter_favourites">मेरे पोस्ट पसंद किए गए</string> + <string name="pref_title_notification_alerts">सूचनाएं</string> + <string name="dialog_mute_hide_notifications">सूचनाएं छिपाएं</string> + <string name="dialog_block_warning">ब्लॉक @%1$s\?</string> + <string name="dialog_title_finishing_media_upload">मीडिया अपलोड समाप्त हो रहा</string> + <string name="confirmation_unmuted">उपयोगकर्ता अनम्यूट किए गए</string> + <string name="confirmation_unblocked">उपयोगकर्ता अनब्लॉक किए गए</string> + <string name="send_media_to">मीडिया को साझा करें …</string> + <string name="send_post_content_to">टूट को साझा करें …</string> + <string name="send_post_link_to">टूट यूआरएल को साझा करें …</string> + <string name="action_share_as">साझा करें …</string> + <string name="action_open_as">%1$s के रूप में खोलें</string> + <string name="download_image">%1$s डाउनलोड हो रहा है</string> + <string name="action_links">लिंक</string> + <string name="action_unmute_desc">अनम्यूट %1$s</string> + <string name="action_view_domain_mutes">छिपे हुए डोमेन</string> + <string name="action_view_blocks">रोके गए उपयोगकर्ता</string> + <string name="action_view_mutes">म्यूट किए गए उपयोगकर्ता</string> + <string name="action_view_favourites">पसंदीदा</string> + <string name="action_logout_confirm">क्या आप %1$s खाते से लॉग आउट करना चाहते हैं\?</string> + <string name="action_compose">लिखें</string> + <string name="action_unfavourite">नापसन्द</string> + <string name="action_favourite">पसंद</string> + <string name="title_edit_profile">अपनी प्रोफाइल एडिट करें</string> + <string name="title_follow_requests">फ़ॉलो अनुरोध</string> + <string name="title_blocks">रोके गए उपयोगकर्ता</string> + <string name="notification_follow_description">नए फ़ॉलोअर की सूचनाएं</string> + <string name="notification_follow_name">नए फ़ॉलोअर</string> + <string name="post_privacy_followers_only">केवल फ़ॉलोअर के लिए</string> + <string name="pref_title_notification_filter_follows">ने फ़ॉलो किया</string> + <string name="dialog_unfollow_warning">इस खाते को अनफ़ॉलो करें\?</string> + <string name="dialog_message_cancel_follow_request">फ़ॉलो अनुरोध को रद्द करें\?</string> + <string name="action_view_follow_requests">फ़ॉलो अनुरोध</string> + <string name="notification_follow_request_format">%1$s ने आपको फ़ॉलो करने का अनुरोध किया</string> + <string name="notification_follow_format">%1$s ने आपको फ़ॉलो किया</string> + <string name="pref_title_show_boosts">बूस्ट दिखाएं</string> + <string name="action_open_reblogged_by">बूस्ट दिखाएं</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> ने पसंद किया</item> + <item quantity="other"><b>%1$s</b> ने पसंद किया</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d सेकेंड शेष</item> + <item quantity="other">%1$d सेकेंड शेष</item> + </plurals> + <string name="error_compose_character_limit">पोस्ट बहुत लंबा है!</string> + <string name="error_failed_app_registration">उस सर्वर से प्रमाणित करने में विफल।</string> + <string name="error_multimedia_size_limit">वीडियो और ऑडियो फ़ाइलों का आकार %1$s एमबी से अधिक नहीं हो सकता है।</string> + <string name="error_image_edit_failed">फोटो संपादित नहीं किया जा सका।</string> + <string name="error_media_upload_image_or_video">फोटो और वीडियो दोनों को एक ही पोस्ट से अटैच नहीं किया जा सकता है।</string> + <string name="error_loading_account_details">खाता विवरण लोड करने में विफल रहा</string> + <string name="error_could_not_load_login_page">लॉगिन पेज लोड नहीं किया जा सका।</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml new file mode 100644 index 0000000..2ab00f9 --- /dev/null +++ b/app/src/main/res/values-hu/strings.xml @@ -0,0 +1,713 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Hiba történt.</string> + <string name="error_network">Hálózati hiba történt. Kérjük, ellenőrizd a kapcsolatot, és próbáld meg újra.</string> + <string name="error_empty">Ez nem lehet üres.</string> + <string name="error_invalid_domain">Helytelen domain</string> + <string name="error_failed_app_registration">Sikertelen hitelesítés ezen a példányon. Ha ez továbbra is fennáll, próbáld a Bejelentkezés Böngészőben opciót a menüben.</string> + <string name="error_no_web_browser_found">Nem található használható böngésző.</string> + <string name="error_authorization_unknown">Azonosítatlan hitelesítési hiba történt. Ha ez továbbra is fennáll, próbáld a Bejelentkezés Böngészőben opciót a menüben.</string> + <string name="error_authorization_denied">Engedély megtagadva. Ha biztos vagy benne, hogy a megfelelő hitelesítési adatokat adtad meg, próbáld a Bejelentkezés Böngészőben funkciót a menüben.</string> + <string name="error_retrieving_oauth_token">Bejelentkezési token megszerzése sikertelen. Ha ez továbbra is fennáll, próbáld a Bejelentkezés Böngészőben opciót a menüben.</string> + <string name="error_compose_character_limit">Túl hosszú a bejegyzés!</string> + <string name="error_media_upload_type">Ilyen típusú fájlt nem lehet feltölteni.</string> + <string name="error_media_upload_opening">Fájl megnyitása sikertelen.</string> + <string name="error_media_upload_permission">Média olvasási engedély szükséges.</string> + <string name="error_media_download_permission">Média tárolási engedély szükséges.</string> + <string name="error_media_upload_image_or_video">Képek és videók egyszerre nem csatolhatóak ugyanazon bejegyzéshez.</string> + <string name="error_media_upload_sending">Feltöltés sikertelen.</string> + <string name="error_sender_account_gone">Nem sikerült elküldeni a bejegyzést.</string> + <string name="title_home">Kezdőlap</string> + <string name="title_notifications">Értesítések</string> + <string name="title_public_local">Helyi</string> + <string name="title_public_federated">Föderációs</string> + <string name="title_direct_messages">Közvetlen üzenetek</string> + <string name="title_tab_preferences">Lapok</string> + <string name="title_view_thread">Szál</string> + <string name="title_posts">Bejegyzések</string> + <string name="title_posts_with_replies">Válaszokkal</string> + <string name="title_posts_pinned">Kitűzött</string> + <string name="title_follows">Követett</string> + <string name="title_followers">Követő</string> + <string name="title_favourites">Kedvencek</string> + <string name="title_mutes">Némított felhasználók</string> + <string name="title_blocks">Letiltott felhasználók</string> + <string name="title_follow_requests">Követési kérelmek</string> + <string name="title_edit_profile">Profilod szerkesztése</string> + <string name="title_drafts">Piszkozatok</string> + <string name="title_licenses">Licenszek</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s megtolta</string> + <string name="post_sensitive_media_title">Érzékeny tartalom</string> + <string name="post_media_hidden_title">Rejtett média</string> + <string name="post_sensitive_media_directions">Kattints a megtekintéshez</string> + <string name="post_content_warning_show_more">Mutass többet</string> + <string name="post_content_warning_show_less">Mutass kevesebbet</string> + <string name="post_content_show_more">Kibontás</string> + <string name="post_content_show_less">Összecsukás</string> + <string name="message_empty">Nincs itt semmi.</string> + <string name="footer_empty">Üres tartalom. Húzd le a frissítéshez!</string> + <string name="notification_reblog_format">%1$s megtolta a bejegyzésedet</string> + <string name="notification_favourite_format">%1$s kedvencnek jelölte a bejegyzésedet</string> + <string name="notification_follow_format">%1$s bekövetett</string> + <string name="report_username_format">\@%1$s jelentése</string> + <string name="report_comment_hint">Egyéb megjegyzés?</string> + <string name="action_quick_reply">Gyors válasz</string> + <string name="action_reply">Válasz</string> + <string name="action_reblog">Megtolás</string> + <string name="action_favourite">Kedvencnek jelölés</string> + <string name="action_more">Több</string> + <string name="action_compose">Szerkesztés</string> + <string name="action_login">Bejelentkezés Tuskyval</string> + <string name="action_logout">Kijelentkezés</string> + <string name="action_logout_confirm">Biztosan ki szeretnél jelentkezni a következőből: %1$s\? A fiók minden helyi adata vázlatokkal és beállításokkal együtt törlődni fog.</string> + <string name="action_follow">Követés</string> + <string name="action_unfollow">Követés vége</string> + <string name="action_block">Letiltás</string> + <string name="action_unblock">Letiltás feloldása</string> + <string name="action_hide_reblogs">Megtolások elrejtése</string> + <string name="action_show_reblogs">Megtolások megjelenítése</string> + <string name="action_report">Bejelentés</string> + <string name="action_delete">Törlés</string> + <string name="action_send">TÜLK</string> + <string name="action_send_public">TÜLK!</string> + <string name="action_retry">Próbáld újra</string> + <string name="action_close">Bezárás</string> + <string name="action_view_profile">Profil</string> + <string name="action_view_preferences">Beállítások</string> + <string name="action_view_account_preferences">Fiókbeállítások</string> + <string name="action_view_favourites">Kedvencek</string> + <string name="action_view_mutes">Némított felhasználók</string> + <string name="action_view_blocks">Letiltott felhasználók</string> + <string name="action_view_follow_requests">Követési kérelmek</string> + <string name="action_view_media">Média</string> + <string name="action_open_in_web">Megnyitás böngészőben</string> + <string name="action_add_media">Média csatolása</string> + <string name="action_photo_take">Kép készítése</string> + <string name="action_share">Megosztás</string> + <string name="action_mute">Némítás</string> + <string name="action_unmute">Némítás feloldása</string> + <string name="action_mention">Megemlítés</string> + <string name="action_hide_media">Média elrejtése</string> + <string name="action_open_drawer">Menü megnyitása</string> + <string name="action_save">Mentés</string> + <string name="action_edit_profile">Profil szerkesztése</string> + <string name="action_edit_own_profile">Szerkesztés</string> + <string name="action_undo">Visszavonás</string> + <string name="action_accept">Elfogadás</string> + <string name="action_reject">Elutasítás</string> + <string name="action_search">Keresés</string> + <string name="action_access_drafts">Piszkozatok</string> + <string name="action_toggle_visibility">Bejegyzés láthatósága</string> + <string name="action_content_warning">Tartalom figyelmeztetés</string> + <string name="action_emoji_keyboard">Emodzsi billentyűzet</string> + <string name="action_add_tab">Fül hozzáadása</string> + <string name="action_links">Hivatkozások</string> + <string name="action_mentions">Említések</string> + <string name="action_hashtags">Hashtagek</string> + <string name="action_open_faved_by">Kedvencek megjelenítése</string> + <string name="title_hashtags_dialog">Hashtagek</string> + <string name="title_mentions_dialog">Említések</string> + <string name="title_links_dialog">Hivatkozások</string> + <string name="download_image">%1$s letöltése</string> + <string name="action_copy_link">Hivatkozás másolása</string> + <string name="action_open_as">Megnyitás mint %1$s</string> + <string name="action_share_as">Megosztás mint …</string> + <string name="send_post_link_to">Bejegyzés webcímének megosztása…</string> + <string name="send_post_content_to">Bejegyzés megosztása…</string> + <string name="confirmation_reported">Elküldve!</string> + <string name="confirmation_unblocked">Felhasználó letiltása feloldva</string> + <string name="confirmation_unmuted">Felhasználó némítása feloldva</string> + <string name="hint_domain">Melyik példány\?</string> + <string name="hint_compose">Mi jár a fejedben\?</string> + <string name="hint_content_warning">Tartalom figyelmeztetés</string> + <string name="hint_display_name">Megjelenítési név</string> + <string name="hint_note">Bemutatkozás</string> + <string name="hint_search">Keresés…</string> + <string name="search_no_results">Nincs találat</string> + <string name="label_quick_reply">Válasz…</string> + <string name="label_avatar">Profilkép</string> + <string name="label_header">Fejléc</string> + <string name="link_whats_an_instance">Mi az a példány\?</string> + <string name="login_connection">Csatlakozás…</string> + <string name="dialog_whats_an_instance">Bármely példány címét vagy domain nevét beírhatod ide, mint a mastodon.social, az icosahedron.website, a social.tchncs.de és <a href="https://instances.social">mások!</a> +\n +\nHa még nincs fiókod, beírhatod annak a példánynak a címét, amelyhez csatlakoznál, majd azon létrehozhatsz egy fiókot. +\n +\nA példány az a hely, ahol a fiókadataidat tárolják, de ettől még ugyanúgy kommunikálhatsz más példányokon lévő emberekkel, mintha ugyanazon az oldalon lennétek. +\n +\nTöbb információt találhatsz itt: <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="dialog_title_finishing_media_upload">Média feltöltés befejezése</string> + <string name="dialog_message_uploading_media">Feltöltés…</string> + <string name="dialog_download_image">Letöltés</string> + <string name="dialog_message_cancel_follow_request">Visszavonod a követési kérelmet?</string> + <string name="dialog_unfollow_warning">Követés megszüntetése?</string> + <string name="dialog_delete_post_warning">Törlöd ezt a bejegyzést\?</string> + <string name="visibility_public">Nyilvános: Bejegyzés nyilvános idővonalra</string> + <string name="visibility_unlisted">Listázatlan: Nem jelenik meg a nyilvános idővonalon</string> + <string name="visibility_private">Csak követőknek: Bejegyzés csak követőknek</string> + <string name="visibility_direct">Közvetlen: Bejegyzés csak a megemlített felhasználóknak</string> + <string name="pref_title_edit_notification_settings">Értesítések</string> + <string name="pref_title_notifications_enabled">Értesítések</string> + <string name="pref_title_notification_alerts">Figyelmeztetések</string> + <string name="pref_title_notification_alert_sound">Értesítés hanggal</string> + <string name="pref_title_notification_alert_vibrate">Értesítés rezgéssel</string> + <string name="pref_title_notification_alert_light">Értesítés fénnyel</string> + <string name="pref_title_notification_filters">Értesítsen, ha</string> + <string name="pref_title_notification_filter_mentions">megemlítettek</string> + <string name="pref_title_notification_filter_follows">bekövettek</string> + <string name="pref_title_notification_filter_reblogs">bejegyzésemet megtolták</string> + <string name="pref_title_notification_filter_favourites">bejegyzésemet kedvencnek jelölték</string> + <string name="pref_title_appearance_settings">Megjelenés</string> + <string name="pref_title_timelines">Idővonalak</string> + <string name="app_them_dark">Sötét</string> + <string name="app_theme_light">Világos</string> + <string name="app_theme_black">Fekete</string> + <string name="app_theme_auto">Automatikus naplementekor</string> + <string name="pref_title_browser_settings">Böngésző</string> + <string name="pref_title_custom_tabs">Hivatkozások megnyitása az alkalmazáson belül</string> + <string name="pref_title_post_filter">Idővonal szűrése</string> + <string name="pref_title_post_tabs">Saját idővonal</string> + <string name="pref_title_show_boosts">Megtolások megjelenítése</string> + <string name="pref_title_show_replies">Válaszok megjelenítése</string> + <string name="pref_title_show_media_preview">Médiaelőnézet megjelenítése</string> + <string name="pref_title_proxy_settings">Proxy</string> + <string name="pref_title_http_proxy_settings">HTTP proxy</string> + <string name="pref_title_http_proxy_enable">HTTP proxy engedélyezése</string> + <string name="pref_title_http_proxy_server">HTTP proxy szerver</string> + <string name="pref_title_http_proxy_port">HTTP Proxy port</string> + <string name="pref_default_post_privacy">Bejegyzések alapértelmezett láthatósága</string> + <string name="pref_default_media_sensitivity">Minden média kényesnek jelölése</string> + <string name="pref_failed_to_sync">A beállítások szinkronizálása nem sikerült</string> + <string name="post_privacy_public">Nyilvános</string> + <string name="post_privacy_unlisted">Listázatlan</string> + <string name="post_privacy_followers_only">Csak követőknek</string> + <string name="pref_post_text_size">Bejegyzés szövegének mérete</string> + <string name="post_text_size_smallest">Legkisebb</string> + <string name="post_text_size_small">Kicsi</string> + <string name="post_text_size_medium">Közepes</string> + <string name="post_text_size_large">Nagy</string> + <string name="post_text_size_largest">Legnagyobb</string> + <string name="notification_mention_name">Új említések</string> + <string name="notification_mention_descriptions">Értesítések új említések esetén</string> + <string name="notification_follow_name">Új követők</string> + <string name="notification_follow_description">Értesítések új követőkről</string> + <string name="notification_boost_name">Megtolások</string> + <string name="notification_boost_description">Értesítések a bejegyzéseid megtolásáról</string> + <string name="notification_favourite_name">Kedvencek</string> + <string name="notification_favourite_description">Értesítések a bejegyzéseid kedvencnek jelöléséről</string> + <string name="notification_mention_format">%1$s megemlített téged</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s és még %4$d</string> + <string name="notification_summary_medium">%1$s, %2$s meg %3$s</string> + <string name="notification_summary_small">%1$s és %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d új interakció</item> + <item quantity="other">%1$d új interakció</item> + </plurals> + <string name="description_account_locked">Zárolt fiók</string> + <string name="about_title_activity">Rólunk</string> + <string name="about_tusky_version">Tusky %1$s</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">Projekt honlapja: https://tusky.app</string> + <string name="about_bug_feature_request_site">Hibajelentés & új funkciók igénylése: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Tusky profilja</string> + <string name="post_share_content">Bejegyzés tartalmának megosztása</string> + <string name="post_share_link">Bejegyzés hivatkozásának megosztása</string> + <string name="post_media_images">Képek</string> + <string name="post_media_video">Videó</string> + <string name="state_follow_requested">Követés kérelmezve</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="follows_you">Követ téged</string> + <string name="pref_title_alway_show_sensitive_media">Mindig mutassa a kényes tartalmat</string> + <string name="title_media">Média</string> + <string name="load_more_placeholder_text">több betöltése</string> + <string name="add_account_name">Fiók hozzáadása</string> + <string name="add_account_description">Új Mastodon-fiók hozzáadása</string> + <string name="action_lists">Listák</string> + <string name="title_lists">Listák</string> + <string name="action_remove">Törlés</string> + <string name="lock_account_label">Fiók zárolása</string> + <string name="compose_save_draft">Elmented a piszkozatot\?</string> + <string name="send_post_notification_title">Bejegyzés küldése…</string> + <string name="send_post_notification_error_title">A bejegyzés elküldése sikertelen</string> + <string name="send_post_notification_channel_name">Bejegyzések elküldése</string> + <string name="send_post_notification_cancel_title">Küldés megszakítva</string> + <string name="send_post_notification_saved_content">A bejegyzés másolatát elmentettük a piszkozataid közé</string> + <string name="action_compose_shortcut">Szerkesztés</string> + <string name="error_no_custom_emojis">A(z) %1$s példánynak nincsenek egyéni emodzsijai</string> + <string name="emoji_style">Emodzsik stílusa</string> + <string name="system_default">Rendszer alapértelmezése</string> + <string name="download_fonts">Először le kell töltened ezeket az emodzsikészleteket</string> + <string name="performing_lookup_title">Keresés…</string> + <string name="action_open_post">Bejegyzés megnyitása</string> + <string name="restart_required">Az app újraindítása szükséges</string> + <string name="restart_emoji">A beállítások érvényesítéséhez újra kell indítani a Tuskyt</string> + <string name="later">Később</string> + <string name="restart">Újraindítás</string> + <string name="caption_systememoji">Az eszközöd alapértelmezett emodzsikészlete</string> + <string name="caption_blobmoji">Az Android 4.4–7.1 Blob emodzsijai</string> + <string name="caption_twemoji">A Mastodon alapértelmezett emodzsikészlete</string> + <string name="download_failed">Letöltés sikertelen</string> + <string name="profile_badge_bot_text">Bot</string> + <string name="account_moved_description">%1$s elköltözött:</string> + <string name="license_description">A Tusky a következő nyílt forráskódú projektekből tartalmaz programkódot és más elemeket:</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Profil metaadatok</string> + <string name="profile_metadata_add">adatok hozzáadása</string> + <string name="profile_metadata_label_label">Címke</string> + <string name="profile_metadata_content_label">Tartalom</string> + <string name="pref_title_absolute_time">Abszolút idő használata</string> + <string name="label_remote_account">Az alábbi felhasználói profil adatok hiányosak lehetnek. Kattints ide a teljes profil böngészőben való megnyitásához.</string> + <string name="title_reblogged_by">Megtolta</string> + <string name="title_favourited_by">Kedvencnek jelölte</string> + <string name="conversation_2_recipients">%1$s és %2$s</string> + <string name="description_post_media_no_description_placeholder">Nincs leírás</string> + <string name="description_visibility_public">Nyilvános</string> + <string name="description_visibility_private">Követők</string> + <string name="action_unfavourite">Kedvenc eltávolítása</string> + <string name="action_delete_and_redraft">Törlés és újrafogalmazás</string> + <string name="action_open_media_n">Média megnyitása: #%1$d</string> + <string name="download_media">Média letöltése</string> + <string name="downloading_media">Média letöltése</string> + <string name="send_media_to">Média megosztása következővel…</string> + <string name="dialog_redraft_post_warning">Törlöd és újraírod ezt a bejegyzést\?</string> + <string name="pref_title_notification_filter_poll">befejeződött egy szavazás</string> + <string name="pref_title_timeline_filters">Szűrők</string> + <string name="app_theme_system">Rendszer téma használata</string> + <string name="pref_title_language">Nyelv</string> + <string name="pref_publishing">Közzététel (szerverrel szinkronizált)</string> + <string name="notification_poll_name">Szavazások</string> + <string name="notification_poll_description">Értesítés a befejezett szavazásokról</string> + <string name="about_tusky_license">Tusky ingyenes és nyílt forráskódú szoftver. A GNU General Public License Version 3 érvényes rá, amit itt tekinthetsz meg: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="abbreviated_in_years">%1$d é múlva</string> + <string name="abbreviated_in_days">%1$d n múlva</string> + <string name="abbreviated_in_hours">%1$d ó múlva</string> + <string name="abbreviated_in_minutes">%1$d p múlva</string> + <string name="abbreviated_years_ago">%1$d é</string> + <string name="abbreviated_days_ago">%1$d n</string> + <string name="abbreviated_hours_ago">%1$d ó</string> + <string name="abbreviated_minutes_ago">%1$d p</string> + <string name="abbreviated_seconds_ago">%1$d mp</string> + <string name="replying_to">Válasz @%1$s részére</string> + <string name="pref_title_public_filter_keywords">Nyilvános idővonalak</string> + <string name="pref_title_thread_filter_keywords">Beszélgetések</string> + <string name="filter_addition_title">Szűrő hozzáadása</string> + <string name="filter_edit_title">Szűrő szerkesztése</string> + <string name="filter_dialog_remove_button">Eltávolítás</string> + <string name="filter_dialog_update_button">Frissítés</string> + <string name="filter_add_description">Szűrendő kifejezés</string> + <string name="error_create_list">Nem sikerült a lista létrehozása</string> + <string name="error_rename_list">Nem sikerült a lista frissítése</string> + <string name="error_delete_list">Nem sikerült a lista törlése</string> + <string name="action_create_list">Lista létrehozása</string> + <string name="action_rename_list">Lista frissítése</string> + <string name="action_delete_list">Lista törlése</string> + <string name="title_domain_mutes">Rejtett domainek</string> + <string name="action_unreblog">Megtolás visszavonása</string> + <string name="action_view_domain_mutes">Rejtett domainek</string> + <string name="action_mute_domain">%1$s némítása</string> + <string name="action_open_reblogger">Megtolás szerkesztő megnyitása</string> + <string name="action_open_reblogged_by">Megtolások megjelenítése</string> + <string name="confirmation_domain_unmuted">%1$s elrejtése feloldva</string> + <string name="mute_domain_warning">Biztos, hogy az egész %s domaint le akarod tiltani? Egyetlen nyilvános idővonalon sem fogsz látni semmilyen tartalmat vagy értesítést innen. Az ebből a domainből származó követőidet el fogjuk távolítani.</string> + <string name="mute_domain_warning_dialog_ok">Teljes domain elrejtése</string> + <string name="pref_title_app_theme">Alkalmazástéma</string> + <string name="pref_title_bot_overlay">Botok jelölőjének megjelenítése</string> + <string name="pref_title_animate_gif_avatars">GIF profilképek animálása</string> + <string name="abbreviated_in_seconds">%1$d mp múlva</string> + <string name="filter_dialog_whole_word">Teljes szó</string> + <string name="filter_dialog_whole_word_description">Ha a kulcsszó csak alfanumerikus karakterekből áll, csak teljes szóra fog illeszkedni</string> + <string name="hint_search_people_list">Általad követettek keresése</string> + <string name="action_add_to_list">Fiók hozzáadása a listához</string> + <string name="action_remove_from_list">Fiók eltávolítása a listából</string> + <string name="compose_active_account_description">Bejegyzés mint %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Tartalom leírása látássérültek számára (%1$d karakteres korlát)</item> + <item quantity="other">Tartalom leírása látássérültek számára (%1$d karakteres korlát)</item> + </plurals> + <string name="action_set_caption">Cím beállítása</string> + <string name="lock_account_label_description">Minden követődet külön engedélyezned kell</string> + <string name="expand_collapse_all_posts">Összes bejegyzés kibontása/összecsukása</string> + <string name="caption_notoemoji">A Google jelenlegi emodzsikészlete</string> + <string name="reblog_private">Megtolás az eredeti közönségnek</string> + <string name="unreblog_private">Megtolás visszavonása</string> + <string name="license_apache_2">Apache licenc alatt közzétéve (másolat alább)</string> + <string name="unpin_action">Kitűzés eltávolítása</string> + <string name="pin_action">Kitűzés</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Kedvenc</item> + <item quantity="other"><b>%1$s</b> Kedvenc</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> Megtolás</item> + <item quantity="other"><b>%1$s</b> Megtolás</item> + </plurals> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_more_recipients">%1$s, %2$s és még %3$d</string> + <string name="description_post_media">Média: %1$s</string> + <string name="description_post_cw">Tartalomfigyelmeztetés: %1$s</string> + <string name="description_post_reblogged">Megtolt</string> + <string name="description_post_favourited">Kedvelt</string> + <string name="description_visibility_unlisted">Listázatlan</string> + <string name="description_visibility_direct">Közvetlen</string> + <string name="description_poll">Szavazás válaszokkal: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="hint_list_name">Lista neve</string> + <string name="edit_hashtag_hint">Hashtag # nélkül</string> + <string name="notifications_clear">Törlés</string> + <string name="notifications_apply_filter">Szűrés</string> + <string name="filter_apply">Alkalmaz</string> + <string name="compose_shortcut_long_label">Bejegyzés létrehozása</string> + <string name="compose_shortcut_short_label">Szerkesztés</string> + <string name="notification_clear_text">Biztos, hogy minden értesítésedet véglegesen törlöd\?</string> + <string name="compose_preview_image_description">Műveletek a(z) %1$s képpel</string> + <string name="poll_info_format"> <!-- 15 szavazat • 1 óra maradt --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s szavazat</item> + <item quantity="other">%1$s szavazat</item> + </plurals> + <string name="poll_info_time_absolute">vége %1$s</string> + <string name="poll_info_closed">véget ért</string> + <string name="poll_vote">Szavazás</string> + <string name="poll_ended_voted">Egy szavazás véget ért, melyben részt vettél</string> + <string name="poll_ended_created">Egy szavazás véget ért, melyet te hoztál létre</string> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d nap maradt</item> + <item quantity="other">%1$d nap maradt</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d óra maradt</item> + <item quantity="other">%1$d óra maradt</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d perc maradt</item> + <item quantity="other">%1$d perc maradt</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d másodperc maradt</item> + <item quantity="other">%1$d másodperc maradt</item> + </plurals> + <string name="button_continue">Folytatás</string> + <string name="button_back">Vissza</string> + <string name="button_done">Kész</string> + <string name="report_sent_success">\@%1$s sikeresen bejelentettük</string> + <string name="hint_additional_info">Egyéb megjegyzések</string> + <string name="report_remote_instance">Továbbítás neki %1$s</string> + <string name="failed_report">Nem sikerült a bejelentés</string> + <string name="failed_fetch_posts">Sikertelen a bejegyzések letöltése</string> + <string name="report_description_1">A bejelentést a szervered moderátorának küldjük el. Alább megadhatsz egy magyarázatot arra, hogy miért jelented be ezt a fiókot:</string> + <string name="report_description_remote_instance">A fiók egy másik szerverről származik. Küldjünk oda is egy anonimizált másolatot a bejelentésről\?</string> + <string name="pref_title_alway_open_spoiler">Tartalomfigyelmeztetéssel ellátott bejegyzések kinyitása mindig</string> + <string name="title_accounts">Fiókok</string> + <string name="failed_search">Sikertelen keresés</string> + <string name="action_add_poll">Szavazás hozzáadása</string> + <string name="create_poll_title">Szavazás</string> + <string name="duration_5_min">5 perc</string> + <string name="duration_30_min">30 perc</string> + <string name="duration_1_hour">1 óra</string> + <string name="duration_6_hours">6 óra</string> + <string name="duration_1_day">1 nap</string> + <string name="duration_3_days">3 nap</string> + <string name="duration_7_days">7 nap</string> + <string name="add_poll_choice">Válasz hozzáadása</string> + <string name="poll_allow_multiple_choices">Több lehetőség</string> + <string name="poll_new_choice_hint">Válasz %1$d</string> + <string name="edit_poll">Szerkesztés</string> + <string name="title_scheduled_posts">Időzített bejegyzések</string> + <string name="action_edit">Szerkesztés</string> + <string name="action_access_scheduled_posts">Időzített bejegyzések</string> + <string name="action_schedule_post">Bejegyzés Időzítése</string> + <string name="action_reset_schedule">Visszaállítás</string> + <string name="post_lookup_error_format">Nem találjuk ezt a bejegyzést %1$s</string> + <string name="title_bookmarks">Könyvjelzők</string> + <string name="action_bookmark">Könyvjelzőzés</string> + <string name="action_view_bookmarks">Könyvjelzők</string> + <string name="about_powered_by_tusky">Tusky által hajtva</string> + <string name="description_post_bookmarked">Könyvjelzőzve</string> + <string name="select_list_title">Lista kiválasztása</string> + <string name="list">Lista</string> + <string name="no_drafts">Nincs egy piszkozatod sem.</string> + <string name="no_scheduled_posts">Nincs egy ütemezett bejegyzésed sem.</string> + <string name="warning_scheduling_interval">A Mastodonban a legrövidebb ütemezhető időintervallum 5 perc.</string> + <string name="notification_follow_request_name">Követési kérelmek</string> + <string name="pref_title_confirm_reblogs">Jóváhagyás megjelenítése megtolás előtt</string> + <string name="pref_title_show_cards_in_timelines">Hivatkozás előnézetének megjelenítése az idővonalakon</string> + <string name="pref_title_enable_swipe_for_tabs">Lapok közötti váltás engedélyezése csúsztatással</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s személy</item> + <item quantity="other">%1$s személy</item> + </plurals> + <string name="hashtags">Hashtagek</string> + <string name="add_hashtag_title">Hashtag hozzáadása</string> + <string name="notification_follow_request_description">Értesítések a követési kérésekről</string> + <string name="pref_main_nav_position_option_bottom">Lent</string> + <string name="pref_main_nav_position_option_top">Fent</string> + <string name="pref_main_nav_position">Fő navigálási pozíció</string> + <string name="pref_title_gradient_for_media">Színes homály megjelenítése a rejtett médiánál</string> + <string name="pref_title_notification_filter_follow_requests">követni szeretnének</string> + <string name="dialog_mute_hide_notifications">Értesítések elrejtése</string> + <string name="dialog_block_warning">Letiltod: @%1$s\?</string> + <string name="dialog_mute_warning">Elnémítsuk @%1$s fiókot\?</string> + <string name="action_unmute_conversation">Beszélgetés némításának feloldása</string> + <string name="action_mute_conversation">Beszélgetés némítása</string> + <string name="action_unmute_domain">%1$s némításának feloldása</string> + <string name="action_unmute_desc">%1$s némításának feloldása</string> + <string name="notification_follow_request_format">%1$s kéri, hogy követhessen</string> + <string name="pref_title_hide_top_toolbar">Felső eszköztár címének elrejtése</string> + <string name="account_note_saved">Elmentve!</string> + <string name="account_note_hint">Saját, mások számára nem látható megjegyzés erről a fiókról</string> + <string name="no_announcements">Nincsenek közlemények.</string> + <string name="title_announcements">Közlemények</string> + <string name="drafts_post_reply_removed">A bejegyzést, melyre válaszul piszkozatot készítettél törölték</string> + <string name="draft_deleted">Piszkozat törölve</string> + <string name="drafts_failed_loading_reply">Nem sikerült a Válasz információit betölteni</string> + <string name="drafts_post_failed_to_send">Ezt a bejegyzést nem tudtuk elküldeni!</string> + <string name="dialog_delete_list_warning">Tényleg le akarod törölni a %1$s listát\?</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Nem tölthetsz fel %1$d médiacsatolmányból többet.</item> + <item quantity="other">Nem tölthetsz fel %1$d médiacsatolmányból többet.</item> + </plurals> + <string name="wellbeing_hide_stats_profile">Profilok mérőszámainak elrejtése</string> + <string name="wellbeing_hide_stats_posts">Bejegyzések mérőszámainak elrejtése</string> + <string name="limit_notifications">Idővonali értesítések korlátozása</string> + <string name="review_notifications">Értesítések áttekintése</string> + <string name="wellbeing_mode_notice">Néhány információ, mely befolyásolhatja a mentális jóllétedet, rejtve marad. Ilyenek például: +\n +\n - Kedvencek, megtolások és követések értesítései +\n - Kedvenc- és megtolásszámlálók a bejegyzéseken +\n - Követő- és bejegyzésstatisztikák a profilokon +\n +\nA leküldéses értesítéseket ez nem befolyásolja, de kézzel átnézheted az értesítési beállításaidat.</string> + <string name="duration_indefinite">Végtelen</string> + <string name="label_duration">Időtartam</string> + <string name="post_media_attachments">Csatolmányok</string> + <string name="post_media_audio">Audio</string> + <string name="notification_subscription_description">Értesítések az általam követettek új bejegyzéseiről</string> + <string name="notification_subscription_name">Új bejegyzések</string> + <string name="pref_title_notification_filter_subscriptions">valaki, akit követek új bejegyzést tett közzé</string> + <string name="notification_subscription_format">%1$s épp bejegyzést írt</string> + <string name="pref_title_wellbeing_mode">Jóllét</string> + <string name="pref_title_animate_custom_emojis">Egyéni emodzsik animálása</string> + <string name="action_unsubscribe_account">Leiratkozás</string> + <string name="action_subscribe_account">Feliratkozás</string> + <string name="follow_requests_info">Bár a fiókod nincs zárolva, a %1$s csapata úgy gondolta, hogy ezen fiókok követési kérelmeit átnéznéd.</string> + <string name="dialog_delete_conversation_warning">Töröljük ezt a beszélgetést\?</string> + <string name="action_delete_conversation">Beszélgetés törlése</string> + <string name="action_unbookmark">Könyvjelző törlése</string> + <string name="pref_title_confirm_favourites">Jóváhagyás megjelenítése kedvencnek jelölés előtt</string> + <string name="notification_update_format">%1$d szerkesztette a bejegyzését</string> + <string name="pref_title_notification_filter_updates">szerkesztették a bejegyzést, mellyel dolgod volt</string> + <string name="notification_sign_up_format">%1$s regisztrált</string> + <string name="pref_title_notification_filter_sign_ups">valaki regisztrált</string> + <string name="notification_sign_up_name">Regisztrációk</string> + <string name="notification_sign_up_description">Értesítések az új felhasználókról</string> + <string name="duration_14_days">14 nap</string> + <string name="duration_30_days">30 nap</string> + <string name="duration_60_days">60 nap</string> + <string name="duration_90_days">90 nap</string> + <string name="duration_180_days">180 nap</string> + <string name="duration_365_days">365 nap</string> + <string name="notification_update_name">Bejegyzések szerkesztése</string> + <string name="notification_update_description">Értesítések olyan bejegyzések szerkesztéséről, melyekkel már dolgod volt</string> + <string name="tusky_compose_post_quicksetting_label">Bejegyzés Létrehozása</string> + <string name="title_migration_relogin">Bejelentkezés újra a leküldési értesítések érdekében</string> + <string name="action_dismiss">Elvetés</string> + <string name="action_details">Részletek</string> + <string name="account_date_joined">Csatlakozva %1$s</string> + <string name="tips_push_notification_migration">Bejelentkezés újra minden fiókkal a leküldéses értesítések engedélyezése érdekében.</string> + <string name="title_login">Bejelentkezés</string> + <string name="status_count_one_plus">1+</string> + <string name="error_could_not_load_login_page">Nem tudtuk betölteni a bejelentkező oldalt.</string> + <string name="saving_draft">Vázlat mentése…</string> + <string name="dialog_push_notification_migration">Ahhoz, hogy a UnifiedPush szolgáltatás révén leküldéses értesítéseket használhass, a Tuskynak fel kell iratkoznia az értesítésekre a Mastodon-kiszolgálódon. Ehhez új bejelentkezésre van szükség, hogy a Tusky számára kiosztott OAuth jogosultságok megváltozzanak. Az újbóli bejelentkezés funkció használata (itt vagy a fiókbeállításokban) megőrzi a helyi piszkozataidat és a gyorsítótár tartalmát.</string> + <string name="dialog_push_notification_migration_other_accounts">Újra bejelentkeztél a fiókodba, hogy feliratkoztasd a Tuskyt a leküldéses értesítések használatára. Ugyanakkor vannak még fiókjaid, melyek még nem lettek így átköltöztetve. Válts át rájuk, és jelentkezz be újra mindegyikben, hogy ezekben is engedélyezd a UnifiedPush értesítések támogatását.</string> + <string name="action_edit_image">Kép szerkesztése</string> + <string name="error_image_edit_failed">A kép nem szerkeszthető.</string> + <string name="error_loading_account_details">Nem sikerült betölteni a fiókadatokat</string> + <string name="pref_title_show_self_username">Felhasználónév megjelenítése az eszköztáron</string> + <string name="delete_scheduled_post_warning">Töröljük ezt az időzített bejegyzést\?</string> + <string name="set_focus_description">Koppintsd vagy húzd a kört, hogy kijelöld azt a fókuszpontot, mely mindig látható lesz az előnézetekben.</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="error_multimedia_size_limit">Video és audio állományok mérete nem lehet %1$s MB-nál nagyobb.</string> + <string name="description_post_language">Bejegyzés nyelve</string> + <string name="pref_show_self_username_always">Mindig</string> + <string name="pref_show_self_username_disambiguate">Ha több fiók is be van jelentkezve</string> + <string name="pref_show_self_username_never">Soha</string> + <string name="duration_no_change">(Nincs változás)</string> + <string name="action_set_focus">Fókuszpont beállítása</string> + <string name="error_following_hashtag_format">Hiba a #%1$s követésekor</string> + <string name="error_unfollowing_hashtag_format">Hiba a #%1$s követésének befejezésekor</string> + <string name="action_add_reaction">reakció hozzáadása</string> + <string name="instance_rule_info">A bejelentkezéssel elfogadod a %1$s szabályait.</string> + <string name="instance_rule_title">%1$s szabályai</string> + <string name="compose_save_draft_loses_media">Elmentsük a vázlatot\? (A mellékleteket újra feltöltjük, amikor a vázlatot visszaállítod.)</string> + <string name="failed_to_pin">Nem sikerült kitűzni</string> + <string name="failed_to_unpin">Nem sikerült a kitűzés visszavonása</string> + <string name="action_add_or_remove_from_list">Hozzáadás vagy törlés a listáról</string> + <string name="no_lists">Nincs egy listád se.</string> + <string name="action_unfollow_hashtag_format">#%1$s követésének vége\?</string> + <string name="hint_media_description_missing">A médiához leírást is kell adni.</string> + <string name="pref_title_notification_filter_reports">van egy új bejelentés</string> + <string name="pref_default_post_language">Bejegyzések alapértelmezett nyelve</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="report_category_violation">Szabálysértés</string> + <string name="pref_title_http_proxy_port_message">A portnak %1$d és %2$d között kell lennie</string> + <string name="status_created_at_now">most</string> + <string name="description_post_edited">Szerkesztve</string> + <string name="error_following_hashtags_unsupported">Ez a kiszolgáló nem támogatja a hashtagek követését.</string> + <string name="error_muting_hashtag_format">Hiba #%1$s némításakor</string> + <string name="error_unmuting_hashtag_format">Hiba #%1$s némításának feloldása közben</string> + <string name="notification_report_format">Új bejelentés a %1$s-n</string> + <string name="notification_header_report_format">%1$s bejelentette %2$s-t</string> + <string name="notification_summary_report_format">%1$s · %2$d bejegyzés csatolva</string> + <string name="confirmation_hashtag_unfollowed">#%1$s kikövetve</string> + <string name="notification_report_name">Bejelentések</string> + <string name="notification_report_description">Értesítések moderációs bejelentésekről</string> + <string name="failed_to_add_to_list">Nem sikerült a fiókot a listához adni</string> + <string name="failed_to_remove_from_list">Nem sikerült a fiókot levenni a listáról</string> + <string name="report_category_spam">Spam</string> + <string name="report_category_other">Egyéb</string> + <string name="post_edited">%1$s szerkesztve</string> + <string name="error_status_source_load">Nem sikerült letölteni a kiszolgálóról az állapotforrást.</string> + <string name="title_followed_hashtags">Követett hashtagek</string> + <string name="a11y_label_loading_thread">Szál betöltése</string> + <string name="pref_title_reading_order">Olvasási sorrend</string> + <string name="pref_reading_order_oldest_first">Régebbi elöl</string> + <string name="pref_reading_order_newest_first">Újabb elöl</string> + <string name="mute_notifications_switch">Értesítések némítása</string> + <string name="pref_summary_http_proxy_disabled">Letiltva</string> + <string name="pref_summary_http_proxy_missing"><nincs beállítva></string> + <string name="pref_summary_http_proxy_invalid"><érvénytelen></string> + <string name="title_edits">Szerkesztések</string> + <string name="status_created_info">%1$s-kor létrehozta</string> + <string name="status_edit_info">%1$s-kor szerkesztette</string> + <string name="post_media_alt">ALT</string> + <string name="action_discard">Változtatások elvetése</string> + <string name="action_continue_edit">Szerkesztés folytatása</string> + <string name="compose_unsaved_changes">Elmentetlen változtatásaid vannak.</string> + <string name="action_share_account_link">Fiókhivatkozás megosztása</string> + <string name="action_share_account_username">Fiók felhasználói nevének megosztása</string> + <string name="send_account_link_to">Fiók webcímének megosztása…</string> + <string name="send_account_username_to">Fiók felhasználói nevének megosztása vele…</string> + <string name="account_username_copied">Felhasználónév másolva</string> + <string name="action_post_failed">Feltöltés meghiúsult</string> + <string name="action_post_failed_detail">A bejegyzésed feltöltése meghiúsult, és a vázlatokba mentettük el. +\n +\nVagy a kiszolgálót nem lehetett elérni, vagy visszautasította a bejegyzést.</string> + <string name="action_post_failed_detail_plural">A bejegyzésed feltöltése meghiúsult, és a vázlatokba mentettük el. +\n +\nVagy a kiszolgálót nem lehetett elérni, vagy visszautasította a bejegyzést.</string> + <string name="action_post_failed_show_drafts">Vázlatok megjelenítése</string> + <string name="action_post_failed_do_nothing">Elvetés</string> + <string name="action_browser_login">Bejelentkezés Böngészővel</string> + <string name="description_login">A legtöbb esetben működik. Nem szivárog ki adat más alkalmazások számára.</string> + <string name="description_browser_login">Támogathat más hitelesítési módozatokat is, de ehhez támogatott böngészőre van szükség.</string> + <string name="title_public_trending_hashtags">Népszerű hashtagek</string> + <string name="accessibility_talking_about_tag">%1$d ember beszél a %2$s hashtagről</string> + <string name="total_usage">Összes használat</string> + <string name="total_accounts">Összes fiók</string> + <string name="dialog_follow_hashtag_title">Hashtag követése</string> + <string name="dialog_follow_hashtag_hint">#hashtag</string> + <string name="action_refresh">Frissítés</string> + <string name="notification_unknown_name">Ismeretlen</string> + <string name="ui_error_favourite">Bejegyzés kedvencnek jelölése sikertelen: %1$s</string> + <string name="ui_error_reblog">Bejegyzés megtolása sikertelen: %1$s</string> + <string name="ui_error_vote">Szavazat leadása a szavazásba sikertelen: %1$s</string> + <string name="ui_error_accept_follow_request">Követési kérelem elfogadása sikertelen: %1$s</string> + <string name="status_filtered_show_anyway">Mutatás mindenképpen</string> + <string name="status_filter_placeholder_label_format">Szűrve: %1$s</string> + <string name="pref_title_account_filter_keywords">Profil</string> + <string name="socket_timeout_exception">A kapcsolatfelvétel a kiszolgálóddal túl sokáig tartott</string> + <string name="ui_error_unknown">ismeretlen ok</string> + <string name="ui_error_bookmark">Bejegyzés könyvjelzőzése sikertelen: %1$s</string> + <string name="ui_error_clear_notifications">Értesítések törlése sikertelen: %1$s</string> + <string name="filter_action_warn">Figyelmeztetés</string> + <string name="filter_action_hide">Elrejtés</string> + <string name="filter_description_warn">Elrejtés figyelmeztetéssel</string> + <string name="filter_description_hide">Elrejtés teljesen</string> + <string name="label_filter_action">Szűrőművelet</string> + <string name="ui_success_rejected_follow_request">Követési kérelem letiltva</string> + <string name="hint_filter_title">Saját szűrő</string> + <string name="label_filter_title">Cím</string> + <string name="action_add">Hozzáadás</string> + <string name="filter_keyword_display_format">%1$s (egész világ)</string> + <string name="label_filter_context">Szűrőkontextus</string> + <string name="label_filter_keywords">Szűrendő kulcsszavak vagy kifejezések</string> + <string name="pref_title_show_stat_inline">Bejegyzésstatisztikák megjelenítése az idővonalon</string> + <string name="ui_error_reject_follow_request">Követési kérelem elutasítása sikertelen: %1$s</string> + <string name="ui_success_accepted_follow_request">Követési kérelem elfogadva</string> + <string name="filter_keyword_addition_title">Kulcsszó hozzáadása</string> + <string name="filter_edit_keyword_title">Kulcsszó szerkesztése</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="help_empty_home">Ez a <b>kezdő idővonalad</b>. Megjeleníti a követett fiókok legfrissebb bejegyzéseit. +\n +\nMás fiókokat másik idővonalakon fedezhetsz fel. Például a példányod helyi idővonalán [iconics gmd_group]. Vagy megkeresheted őket név szerint [iconics gmd_search]; például keress rá a Tuskyra, hogy megtaláld a Mastodon-fiókunkat.</string> + <string name="post_media_image">Kép</string> + <string name="select_list_manage">Listák kezelése</string> + <string name="pref_ui_text_size">Felület betűmérete</string> + <string name="notification_listenable_worker_name">Háttértevékenység</string> + <string name="notification_listenable_worker_description">Értesítések, amikor a Tusky a háttérben működik</string> + <string name="notification_notification_worker">Értesítések lekérése…</string> + <string name="notification_prune_cache">Gyorsítótár karbantartása…</string> + <string name="load_newest_notifications">Legújabb értesítések betöltése</string> + <string name="compose_delete_draft">Töröljük a vázlatot\?</string> + <string name="error_missing_edits">A kiszolgálód tudja, hogy ezt a bejegyzést szerkesztették, de erről nincs másolata, így ezt nem tudjuk neked megmutatni. +\n +\nEz egy <a href="https://github.com/mastodon/mastodon/issues/25398">Mastodon hiba #25398</a>.</string> + <string name="reply_sending">Küldés…</string> + <string name="reply_sending_long">A válaszodat épp elküldjük.</string> + <string name="label_image">Kép</string> + <string name="about_account_info_title">Saját fiók</string> + <string name="about_device_info">%1$s %2$s +\nAndroid verzió: %3$s +\nSDK verzió: %4$d</string> + <string name="about_device_info_title">Saját eszközöd</string> + <string name="about_account_info">\@%1$s@%2$s +\nVerzió: %3$s</string> + <string name="about_copy">Verzió és eszköz információk másolása</string> + <string name="about_copied">Verzió és eszköz információ lemásolva</string> + <string name="action_translate">Fordítás</string> + <string name="action_show_original">Eredeti megjelenítése</string> + <string name="label_translating">Fordítás…</string> + <string name="label_translated">Fordítva %1$s forrásból %2$s eszközzel</string> + <string name="list_exclusive_label">Elrejtés a saját idővonalról</string> + <string name="ui_error_translate">Nem lehet lefordítani: %1$s</string> + <string name="error_media_playback">Lejátszás meghiúsult: %1$s</string> + <string name="dialog_delete_filter_text">Szűrő \'%1$s\' törlése\?</string> + <string name="dialog_delete_filter_positive_action">Törlés</string> + <string name="dialog_save_profile_changes_message">El akarod menteni a profilod változásait\?</string> + <string name="app_theme_system_black">Rendszerséma Használata (fekete)</string> + <string name="action_view_filter">Szűrő megtekintése</string> + <string name="following_hashtag_success_format">Nem követjük #%1$s hashtag-et</string> + <string name="unmuting_hashtag_success_format">#%1$s hashtag némításának feloldása</string> + <string name="muting_hashtag_success_format">#%1$s hashtag elnémítása figyelmeztetésként</string> + <string name="unfollowing_hashtag_success_format">Nem követjük tovább a #%1$s hashtag-et</string> + <string name="help_empty_conversations">It találhatóak a <b>privát üzeneteid</b>; melyeket néha beszélgetéseknek vagy közvetlen üzeneteknek hívunk (DM). +\n +\nPrivát üzeneteket az ilyen bejegyzések láthatóságának [iconics gmd_public] publikusról [iconics gmd_mail] <i>Közvetlen</i>re állításával és egy vagy több felhasználó megemlítésével hozhatsz létre. +\n +\nPéldául indulhatsz egy profilon a létrehozásra kattintva [iconics gmd_edit] a láthatóságot megváltoztatva. </string> + <string name="title_public_trending_statuses">Felkapott bejegyzések</string> + <string name="help_empty_lists">Ez a <b>listanézeted</b>. Meghatározhatsz privát listákat és fiókokat adhatsz hozzájuk. +\n +\nMEGJEGYZÉS: Csak olyan fiókot rakhatsz a listádra, melyet követsz is. +\n +\n Ezeket a listákat lapokként használhatod a Fiókbeállítások [iconics gmd_account_circle] [iconics gmd_navigate_next] Lapok alatt. </string> + <string name="error_blocking_domain">Nem sikerült némítani %1$s: %2$s</string> + <string name="error_unblocking_domain">Nem sikerült a némítás feloldása %1$s: %2$s</string> + <string name="error_media_upload_sending_fmt">A feltöltés sikertelen: %1$s</string> + <string name="pref_title_per_timeline_preferences">Idővonalankénti beállítások</string> + <string name="list_reply_policy_none">Senki</string> + <string name="list_reply_policy_list">A lista tagjai</string> + <string name="list_reply_policy_followed">Bármely követett felhasználó</string> + <string name="list_reply_policy_label">Erre való válaszok megjelenítése</string> + <string name="pref_title_show_self_boosts">Önmegtolások megjelenítése</string> + <string name="pref_title_show_self_boosts_description">Valaki a saját bejegyzését tolja meg</string> + <string name="pref_title_show_notifications_filter">Értesítésszűrő megjelenítése</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml new file mode 100644 index 0000000..bd98955 --- /dev/null +++ b/app/src/main/res/values-in/strings.xml @@ -0,0 +1,251 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_empty">Kolom ini tidak boleh kosong.</string> + <string name="error_invalid_domain">Domain yang dimasukkan tidak valid</string> + <string name="error_no_web_browser_found">Tidak dapat menemukan browser untuk digunakan.</string> + <string name="error_authorization_unknown">"Terjadi kesalahan otorisasi yang tidak diketahui. Jika ini berlanjut, coba Masuk di Browser dari menu."</string> + <string name="error_authorization_denied">Otorisasi ditolak. Jika Anda yakin telah memberikan kredensial yang benar, coba Masuk di Browser dari menu.</string> + <string name="error_retrieving_oauth_token">Gagal mendapatkan token masuk. Jika ini terus berlanjut, coba Masuk di Browser dari menu.</string> + <string name="title_login">Masuk</string> + <string name="title_home">Beranda</string> + <string name="title_notifications">Notifikasi</string> + <string name="title_public_local">Lokal</string> + <string name="title_direct_messages">Pesan langsung</string> + <string name="error_multimedia_size_limit">Berkas video dan audio tidak boleh melebihi %1$s MB.</string> + <string name="error_image_edit_failed">Gambar tidak dapat diubah.</string> + <string name="error_media_upload_type">Jenis berkas tersebut tidak dapat diunggah.</string> + <string name="error_media_upload_opening">Berkas itu tidak dapat dibuka.</string> + <string name="error_media_upload_sending">Gagal mengunggah.</string> + <string name="title_tab_preferences">Tab</string> + <string name="title_view_thread">Utas</string> + <string name="error_following_hashtag_format">Terjadi kesalahan saat mengikuti #%1$s</string> + <string name="error_unfollowing_hashtag_format">Terjadi kesalahan saat berhenti mengikuti #%1$s</string> + <string name="error_could_not_load_login_page">Tidak dapat memuat halaman masuk.</string> + <string name="title_posts">Postingan</string> + <string name="title_posts_with_replies">Dengan balasan</string> + <string name="title_posts_pinned">Disematkan</string> + <string name="title_follows">Mengikuti</string> + <string name="title_followers">Pengikut</string> + <string name="title_favourites">Favorit</string> + <string name="title_bookmarks">Markah</string> + <string name="title_blocks">Pengguna diblokir</string> + <string name="title_domain_mutes">Domain tersembunyi</string> + <string name="title_edit_profile">Ubah profil</string> + <string name="title_drafts">Draf</string> + <string name="title_scheduled_posts">Postingan terjadwal</string> + <string name="error_compose_character_limit">Postingan terlalu panjang!</string> + <string name="error_media_upload_permission">Izin untuk membaca media diperlukan.</string> + <string name="error_media_download_permission">Izin untuk menyimpan media diperlukan.</string> + <string name="title_licenses">Lisensi</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_media_hidden_title">Media disembunyikan</string> + <string name="post_sensitive_media_directions">Klik untuk melihat</string> + <string name="post_content_warning_show_more">Tampilkan Lebih Banyak</string> + <string name="post_content_warning_show_less">Tampilkan Lebih Sedikit</string> + <string name="message_empty">Tidak ada apa pun disini.</string> + <string name="notification_follow_format">%1$s mengikuti Anda</string> + <string name="report_username_format">Laporkan @%1$s</string> + <string name="action_follow">Ikuti</string> + <string name="action_unfollow">Berhenti Mengikuti</string> + <string name="action_block">Blokir</string> + <string name="action_unblock">Buka blokir</string> + <string name="action_report">Laporkan</string> + <string name="action_delete">Hapus</string> + <string name="action_retry">Coba lagi</string> + <string name="action_close">Tutup</string> + <string name="action_view_profile">Profil</string> + <string name="action_view_preferences">Preferensi</string> + <string name="action_view_account_preferences">Preferensi Akun</string> + <string name="action_view_favourites">Favorit</string> + <string name="action_view_bookmarks">Markah</string> + <string name="action_view_mutes">Pengguna dibisukan</string> + <string name="action_view_blocks">Pengguna diblokir</string> + <string name="action_view_media">Media</string> + <string name="action_open_in_web">Buka di peramban</string> + <string name="action_add_media">Tambahkan media</string> + <string name="action_photo_take">Ambil gambar</string> + <string name="action_share">Bagikan</string> + <string name="action_mute">Bisukan</string> + <string name="action_unmute_desc">Jangan bisukan %1$s</string> + <string name="action_mute_domain">Bisukan %1$s</string> + <string name="action_unmute_domain">Jangan bisukan %1$s</string> + <string name="action_unmute_conversation">Jangan bisukan percakapan</string> + <string name="action_save">Simpan</string> + <string name="action_edit_profile">Ubah profil</string> + <string name="action_edit_own_profile">Ubah</string> + <string name="action_undo">Batalkan</string> + <string name="action_reject">Tolak</string> + <string name="action_accept">Terima</string> + <string name="action_search">Cari</string> + <string name="action_access_drafts">Draf</string> + <string name="action_reset_schedule">Setel Ulang</string> + <string name="action_links">Tautan</string> + <string name="action_details">Detail</string> + <string name="action_copy_link">Salin tautan</string> + <string name="download_image">Mengunduh %1$s</string> + <string name="download_media">Unduh media</string> + <string name="downloading_media">Mengunduh media</string> + <string name="confirmation_reported">Terkirim!</string> + <string name="hint_compose">Apa yang terjadi\?</string> + <string name="hint_search">Cari…</string> + <string name="login_connection">Menghubungkan…</string> + <string name="dialog_title_finishing_media_upload">Menyelesaikan Pengunggahan Media</string> + <string name="dialog_block_warning">Blokir @%1$s\?</string> + <string name="dialog_mute_warning">Bisukan @%1$s\?</string> + <string name="dialog_delete_conversation_warning">Hapus percakapan ini\?</string> + <string name="pref_title_notification_alert_sound">Beri tahu dengan suara</string> + <string name="pref_title_notification_alert_vibrate">Beri tahu dengan getaran</string> + <string name="pref_title_notification_alert_light">Beri tahu dengan lampu</string> + <string name="app_them_dark">Gelap</string> + <string name="app_theme_light">Terang</string> + <string name="app_theme_black">Hitam</string> + <string name="pref_title_timelines">Linimasa</string> + <string name="pref_title_app_theme">Tema Aplikasi</string> + <string name="pref_title_browser_settings">Peramban</string> + <string name="pref_title_show_replies">Tampilkan balasan</string> + <string name="pref_main_nav_position_option_top">Atas</string> + <string name="pref_main_nav_position_option_bottom">Bawah</string> + <string name="post_privacy_public">Publik</string> + <string name="post_text_size_small">Kecil</string> + <string name="post_text_size_medium">Sedang</string> + <string name="post_text_size_large">Besar</string> + <string name="post_text_size_largest">Terbesar</string> + <string name="pref_show_self_username_always">Selalu</string> + <string name="pref_show_self_username_never">Jangan Pernah</string> + <string name="notification_follow_name">Pengikut Baru</string> + <string name="notification_follow_description">Notifikasi tentang pengikut baru</string> + <string name="notification_sign_up_name">Daftar</string> + <string name="about_title_activity">Tentang</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_powered_by_tusky">Dipersembahkan oleh Tusky</string> + <string name="post_media_video">Video</string> + <string name="post_media_audio">Audio</string> + <string name="post_media_attachments">Lampiran</string> + <string name="status_count_one_plus">1+</string> + <string name="follows_you">Mengikuti Anda</string> + <string name="add_account_description">Tambahkan Akun Mastodon baru</string> + <string name="action_lists">Daftar</string> + <string name="title_lists">Daftar</string> + <string name="error_rename_list">Tidak dapat mengubah nama daftar</string> + <string name="error_delete_list">Tidak dapat menghapus daftar</string> + <string name="action_create_list">Buat daftar</string> + <string name="action_rename_list">Ubah nama daftar</string> + <string name="action_delete_list">Hapus daftar</string> + <string name="lock_account_label">Kunci akun</string> + <string name="system_default">Bawaan sistem</string> + <string name="action_compose_shortcut">Tulis</string> + <string name="restart">Mulai ulang</string> + <string name="account_note_saved">Disimpan!</string> + <string name="error_generic">Terjadi kesalahan.</string> + <string name="error_network">Terjadi kesalahan pada jaringan! Harap periksa koneksi Anda dan coba lagi!</string> + <string name="error_loading_account_details">Gagal memuat detail akun</string> + <string name="error_sender_account_gone">Gagal mengirim postingan.</string> + <string name="title_mutes">Pengguna dibisukan</string> + <string name="title_announcements">Pengumuman</string> + <string name="post_sensitive_media_title">Konten sensitif</string> + <string name="notification_follow_request_format">%1$s meminta untuk mengikuti anda</string> + <string name="action_edit">Ubah</string> + <string name="action_delete_conversation">Hapus percakapan</string> + <string name="action_unmute">Jangan bisukan</string> + <string name="action_mute_conversation">Bisukan percakapan</string> + <string name="action_schedule_post">Jadwalkan Postingan</string> + <string name="action_access_scheduled_posts">Postingan terjadwal</string> + <string name="label_quick_reply">Balas…</string> + <string name="dialog_message_uploading_media">Mengunggah…</string> + <string name="dialog_mute_hide_notifications">Sembunyikan notifikasi</string> + <string name="pref_title_appearance_settings">Penampilan</string> + <string name="pref_title_language">Bahasa</string> + <string name="post_text_size_smallest">Terkecil</string> + <string name="notification_favourite_name">Favorit</string> + <string name="post_media_images">Gambar</string> + <string name="add_account_name">Tambah Akun</string> + <string name="error_create_list">Tidak dapat membuat daftar</string> + <string name="later">Nanti</string> + <string name="error_failed_app_registration">Gagal mengautentikasi dengan instance tersebut. Jika ini berlanjut, coba Masuk di Browser dari menu.</string> + <string name="tusky_compose_post_quicksetting_label">Tulis Postingan</string> + <string name="error_media_upload_image_or_video">Gambar dan video tidak dapat disematkan ke dalam post yang sama.</string> + <string name="title_public_federated">Federasi</string> + <string name="title_migration_relogin">Login ulang untuk notifikasi push</string> + <string name="title_follow_requests">Permintaan mengikuti</string> + <string name="post_boosted_format">%1$s ter-boost</string> + <string name="post_content_show_more">Meluaskan</string> + <string name="footer_empty">Tidak ada apapun di sini. Tarik ke bawah untuk menyegarkan!</string> + <string name="notification_reblog_format">%1$s telah meng-boost post mu</string> + <string name="notification_favourite_format">%1$s memfavoritkan post mu</string> + <string name="notification_sign_up_format">%1$s mendaftar</string> + <string name="notification_subscription_format">%1$s baru saja memosting</string> + <string name="notification_update_format">%1$s mengedit post mereka</string> + <string name="report_comment_hint">Komentar tambahan\?</string> + <string name="action_reply">Balas</string> + <string name="action_quick_reply">Balas Cepat</string> + <string name="action_reblog">Boost</string> + <string name="action_unreblog">Hapus boost</string> + <string name="action_favourite">Favorit</string> + <string name="action_unfavourite">Hapus favorit</string> + <string name="action_unbookmark">Hapus bookmark</string> + <string name="action_more">Lebih</string> + <string name="action_compose">Menyusun</string> + <string name="action_login">Masuk dengan Mastodon</string> + <string name="action_logout">Keluar</string> + <string name="action_logout_confirm">Apakah kamu yakin ingin keluar dari akun %1$s\?</string> + <string name="action_hide_reblogs">Sembunyikan boost</string> + <string name="action_show_reblogs">Tampilkan boost</string> + <string name="action_view_domain_mutes">Domain tersembunyi</string> + <string name="action_view_follow_requests">Permintaan mengikuti</string> + <string name="action_add_poll">Tambah pemilihan</string> + <string name="action_mention">Sebut</string> + <string name="action_hide_media">Sembunyikan media</string> + <string name="action_open_drawer">Buka laci</string> + <string name="action_delete_and_redraft">Hapus dan draf ulang</string> + <string name="action_share_as">Bagikan sebagai…</string> + <string name="send_post_link_to">Bagikan URL post kepada…</string> + <string name="action_toggle_visibility">Visibilitas post</string> + <string name="send_post_content_to">Bagikan post kepada…</string> + <string name="hint_content_warning">Peringatan konten</string> + <string name="hint_display_name">Nama tampilan</string> + <string name="action_content_warning">Peringatan konten</string> + <string name="action_emoji_keyboard">Papan ketik Emoji</string> + <string name="action_add_tab">Tambah Tab</string> + <string name="action_hashtags">Hashtag</string> + <string name="action_open_reblogger">Buka penulis boost</string> + <string name="action_open_reblogged_by">Tampilkan boost</string> + <string name="action_open_faved_by">Tampilkan favorit</string> + <string name="action_dismiss">Menolak</string> + <string name="title_hashtags_dialog">Hashtag</string> + <string name="title_links_dialog">Tautan</string> + <string name="action_open_media_n">Buka media #%1$d</string> + <string name="action_open_as">Buka sebagai %1$s</string> + <string name="send_media_to">Bagikan media kepada…</string> + <string name="confirmation_unblocked">Pengguna ter-unblock</string> + <string name="hint_domain">Instansi yang mana\?</string> + <string name="hint_note">Bio</string> + <string name="search_no_results">Tidak ada hasil</string> + <string name="link_whats_an_instance">Apa itu instansi\?</string> + <string name="dialog_download_image">Unduh</string> + <string name="dialog_message_cancel_follow_request">Tarik kembali permintaan mengikuti\?</string> + <string name="mute_domain_warning_dialog_ok">Sembunyikan keseluruhan domain</string> + <string name="visibility_public">Publik: Post untuk linimasa publik</string> + <string name="visibility_unlisted">Tak terdaftar: Jangan tampilkan di linimasa publik</string> + <string name="visibility_private">Hanya pengikut: Post hanya untuk pengikut</string> + <string name="visibility_direct">Langsung: Post kepada pengguna yang disebut saja</string> + <string name="pref_title_edit_notification_settings">Pemberitahuan</string> + <string name="pref_title_notifications_enabled">Pemberitahuan</string> + <string name="pref_title_notification_alerts">Peringatan</string> + <string name="action_add_reaction">tambah reaksi</string> + <string name="dialog_unfollow_warning">Unfollow akun ini\?</string> + <string name="pref_title_notification_filter_favourites">post ku telah difavoritkan</string> + <string name="dialog_delete_post_warning">Hapus post ini\?</string> + <string name="dialog_redraft_post_warning">Hapus dan draf ulang post ini\?</string> + <string name="pref_title_notification_filters">Beritahu saya ketika</string> + <string name="pref_title_notification_filter_mentions">disebut</string> + <string name="pref_title_notification_filter_follows">diikuti</string> + <string name="pref_title_notification_filter_reblogs">post ku telah di-boost</string> + <string name="pref_title_notification_filter_poll">pemilihan telah berakhir</string> + <string name="pref_title_notification_filter_subscriptions">seseorang yang saya langganani menerbitkan sebuah post baru</string> + <string name="pref_title_notification_filter_sign_ups">seseorang mendaftar</string> + <string name="error_following_hashtags_unsupported">Instance ini tidak mendukung hashtag berikut.</string> + <string name="error_muting_hashtag_format">Kesalahan menonaktifkan #%1$s</string> + <string name="error_unmuting_hashtag_format">Kesalahan mengaktifkan #%1$s</string> + <string name="error_status_source_load">Gagal memuat sumber status dari server.</string> + <string name="title_public_trending_hashtags">Hashtag yang sedang tren</string> +</resources> diff --git a/app/src/main/res/values-is/strings.xml b/app/src/main/res/values-is/strings.xml new file mode 100644 index 0000000..829d993 --- /dev/null +++ b/app/src/main/res/values-is/strings.xml @@ -0,0 +1,706 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="action_login">Skrá inn með Tusky</string> + <string name="link_whats_an_instance">Hvað er tilvik\?</string> + <string name="title_favourites">Eftirlæti</string> + <string name="title_drafts">Drög</string> + <string name="action_logout">Skrá út</string> + <string name="action_view_preferences">Kjörstillingar</string> + <string name="action_view_account_preferences">Eiginleikar notandaaðgangs</string> + <string name="action_edit_profile">Breyta notandasniði</string> + <string name="action_search">Leita</string> + <string name="about_title_activity">Um hugbúnaðinn</string> + <string name="action_lists">Listar</string> + <string name="title_lists">Listar</string> + <string name="error_generic">Villa kom upp.</string> + <string name="error_network">Villa kom upp í netkerfi. Athugaðu nettenginguna þína og prófaðu svo aftur.</string> + <string name="error_empty">Þetta má ekki vera tómt.</string> + <string name="error_invalid_domain">Ógilt lén sett inn</string> + <string name="error_failed_app_registration">Mistókst að auðkenna gagnvart þessu tilviki. Ef þetta er viðvarandi skaltu prófa \'Skrá inn í vafra\' úr valmyndinni.</string> + <string name="error_no_web_browser_found">Gat ekki fundið neinn vafra til að nota.</string> + <string name="error_authorization_unknown">Óskilgreind auðkenningarvilla kom upp. Mistókst að auðkenna gagnvart þessu tilviki. Ef þetta er viðvarandi skaltu prófa \'Skrá inn í vafra\' úr valmyndinni.</string> + <string name="error_authorization_denied">Heimild var hafnað. Mistókst að auðkenna gagnvart þessu tilviki. Ef þetta er viðvarandi skaltu prófa \'Skrá inn í vafra\' úr valmyndinni.</string> + <string name="error_retrieving_oauth_token">Mistókst að fá innskráningarteikn. Mistókst að auðkenna gagnvart þessu tilviki. Ef þetta er viðvarandi skaltu prófa \'Skrá inn í vafra\' úr valmyndinni.</string> + <string name="error_compose_character_limit">Færslan er of löng!</string> + <string name="error_media_upload_type">Þessa tegund skrár er ekki hægt að senda inn.</string> + <string name="error_media_upload_opening">Ekki var hægt að opna skrána.</string> + <string name="error_media_upload_permission">Krafist er heimilda til að lesa gögn.</string> + <string name="error_media_download_permission">Krafist er heimilda til að geyma gögn.</string> + <string name="error_media_upload_image_or_video">Ekki er hægt að hengja bæði myndir og myndskeið við sömu færslu.</string> + <string name="error_media_upload_sending">Innsendingin mistókst.</string> + <string name="error_sender_account_gone">Villa við að senda færslu.</string> + <string name="title_home">Heim</string> + <string name="title_notifications">Tilkynningar</string> + <string name="title_public_local">Staðvært</string> + <string name="title_public_federated">Sameiginlegt</string> + <string name="title_direct_messages">Bein skilaboð</string> + <string name="title_tab_preferences">Flipar</string> + <string name="title_view_thread">Þráður</string> + <string name="title_posts">Færslur</string> + <string name="title_posts_with_replies">Með svörum</string> + <string name="title_posts_pinned">Fest</string> + <string name="title_follows">Fylgist með</string> + <string name="title_followers">Fylgjendur</string> + <string name="title_bookmarks">Bókamerki</string> + <string name="title_mutes">Þaggaðir notendur</string> + <string name="title_blocks">Útilokaðir notendur</string> + <string name="title_domain_mutes">Falin lén</string> + <string name="title_follow_requests">Fylgjendabeiðnir</string> + <string name="title_edit_profile">Breyta notandasniðinu þínu</string> + <string name="title_scheduled_posts">Áætlaðar færslur</string> + <string name="title_licenses">Notkunarleyfi</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s endurbirti</string> + <string name="post_sensitive_media_title">Viðkvæmt efni</string> + <string name="post_media_hidden_title">Myndefni er falið</string> + <string name="post_sensitive_media_directions">Ýttu til að skoða</string> + <string name="post_content_warning_show_more">Sýna meira</string> + <string name="post_content_warning_show_less">Sýna minna</string> + <string name="post_content_show_more">Fletta út</string> + <string name="post_content_show_less">Fella saman</string> + <string name="message_empty">Ekkert hér.</string> + <string name="footer_empty">Ekkert hér. Togaðu niður til að endurhlaða!</string> + <string name="notification_reblog_format">%1$s endurbirti færsluna þína</string> + <string name="notification_favourite_format">%1$s setti færslu frá þér í eftirlæti</string> + <string name="notification_follow_format">%1$s fylgist núna með þér</string> + <string name="report_username_format">Kæra @%1$s</string> + <string name="report_comment_hint">Aðrar athugasemdir\?</string> + <string name="action_quick_reply">Snöggt svar</string> + <string name="action_reply">Svara</string> + <string name="action_reblog">Endurbirta</string> + <string name="action_unreblog">Fjarlægja endurbirtingu</string> + <string name="action_favourite">Eftirlæti</string> + <string name="action_bookmark">Bókamerki</string> + <string name="action_unfavourite">Fjarlægja eftirlæti</string> + <string name="action_more">Meira</string> + <string name="action_compose">Semja skilaboð</string> + <string name="action_logout_confirm">Ertu viss um að þú viljir skrá þig út af notandaaðgangnum %1$s\? Þetta mun eyða öllum staðværum gögnum af aðgangnum, þar með talið drögum og kjörstillingum.</string> + <string name="action_follow">Fylgja</string> + <string name="action_unfollow">Hætta að fylgjast með</string> + <string name="action_block">Útiloka</string> + <string name="action_unblock">Aflétta útilokun</string> + <string name="action_hide_reblogs">Fela endurbirtingar</string> + <string name="action_show_reblogs">Sýna endurbirtingar</string> + <string name="action_report">Tilkynna</string> + <string name="action_edit">Breyta</string> + <string name="action_delete">Eyða</string> + <string name="action_delete_and_redraft">Eyða og endurvinna drög</string> + <string name="action_send">BIRTA</string> + <string name="action_send_public">BIRTA!</string> + <string name="action_retry">Reyna aftur</string> + <string name="action_close">Loka</string> + <string name="action_view_profile">Notandasnið</string> + <string name="action_view_favourites">Eftirlæti</string> + <string name="action_view_bookmarks">Bókamerki</string> + <string name="action_view_mutes">Þaggaðir notendur</string> + <string name="action_view_blocks">Útilokaðir notendur</string> + <string name="action_view_domain_mutes">Falin lén</string> + <string name="action_view_follow_requests">Fylgjendabeiðnir</string> + <string name="action_view_media">Myndefni</string> + <string name="action_open_in_web">Opna í vafra</string> + <string name="action_add_media">Bæta við myndefni</string> + <string name="action_add_poll">Bæta við könnun</string> + <string name="action_photo_take">Taka ljósmynd</string> + <string name="action_share">Deila</string> + <string name="action_mute">Þagga niður</string> + <string name="action_unmute">Ekki þagga</string> + <string name="action_mute_domain">Þagga niður í %1$s</string> + <string name="action_mention">Tilvísun</string> + <string name="action_hide_media">Fela myndefni</string> + <string name="action_open_drawer">Opna sleða</string> + <string name="action_save">Vista</string> + <string name="action_edit_own_profile">Breyta</string> + <string name="action_undo">Afturkalla</string> + <string name="action_accept">Samþykkja</string> + <string name="action_reject">Hafna</string> + <string name="action_access_drafts">Drög</string> + <string name="action_access_scheduled_posts">Áætlaðar færslur</string> + <string name="action_toggle_visibility">Sýnileiki færslu</string> + <string name="action_content_warning">Aðvörun vegna efnis</string> + <string name="action_emoji_keyboard">Lyklaborð með tjáningartáknum</string> + <string name="action_schedule_post">Tímasetja færslu</string> + <string name="action_reset_schedule">Frumstilla</string> + <string name="action_add_tab">Bæta við flipa</string> + <string name="action_links">Tenglar</string> + <string name="action_mentions">Tilvísanir</string> + <string name="action_hashtags">Myllumerki</string> + <string name="action_open_reblogger">Opna höfund endurbirtingar</string> + <string name="action_open_reblogged_by">Sýna endurbirtingar</string> + <string name="action_open_faved_by">Birta eftirlæti</string> + <string name="title_hashtags_dialog">Myllumerki</string> + <string name="title_mentions_dialog">Tilvísanir</string> + <string name="title_links_dialog">Tenglar</string> + <string name="action_open_media_n">Opna myndefni #%1$d</string> + <string name="download_image">Sæki %1$s</string> + <string name="action_copy_link">Afrita tengilinn</string> + <string name="action_open_as">Opna sem %1$s</string> + <string name="action_share_as">Deila sem …</string> + <string name="download_media">Sækja myndefni</string> + <string name="downloading_media">Næ í myndefni</string> + <string name="send_post_link_to">Deila slóð á færslu til…</string> + <string name="send_post_content_to">Deila færslu með…</string> + <string name="send_media_to">Deila myndefni með…</string> + <string name="confirmation_reported">Sent!</string> + <string name="confirmation_unblocked">Hætt að útiloka notanda</string> + <string name="confirmation_unmuted">Hætt að þagga niður í notanda</string> + <string name="confirmation_domain_unmuted">Hætt að fela %1$s</string> + <string name="hint_domain">Hvaða tilvik\?</string> + <string name="hint_compose">Hvað er í gangi hérna\?</string> + <string name="hint_content_warning">Aðvörun vegna efnis</string> + <string name="hint_display_name">Birtingarnafn</string> + <string name="hint_note">Æviágrip</string> + <string name="hint_search">Leita…</string> + <string name="search_no_results">Engar niðurstöður</string> + <string name="label_quick_reply">Svara…</string> + <string name="label_avatar">Auðkennismynd</string> + <string name="label_header">Haus</string> + <string name="login_connection">Tengist…</string> + <string name="dialog_whats_an_instance">Hægt er að setja hér inn vistfang eða lén á hvaða tilviki sem er, svo sem mastodon.social, icosahedron.website, social.tchncs.de og <a href="https://instances.social">fleiri!</a> +\n +\nEf þú ert ekki ennþá með notandaaðgang, geturðu sett inn nafnið á því tilviki sem þú vilt tilheyra og búið til aðgang þar. +\n +\nTilvik er ákveðinn einn vefþjónn þar sem notandaaðgangurinn þinn er hýstur, en eftir sem áður er auðvelt fyrir þig að eiga í samskiptum við fólk og fylgjast með einstaklingum á öðrum tilvikum, rétt eins og þið væruð á sama vefsvæðinu. +\n +\nNánari upplýsingar má finna á <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="dialog_title_finishing_media_upload">Klára innsendingu myndefnis</string> + <string name="dialog_message_uploading_media">Sendi inn…</string> + <string name="dialog_download_image">Sækja</string> + <string name="dialog_message_cancel_follow_request">Afturkalla beiðni um að fylgjast með\?</string> + <string name="dialog_unfollow_warning">Hætta að fylgjast með þessum aðgangi\?</string> + <string name="dialog_delete_post_warning">Eyða þessari færslu\?</string> + <string name="dialog_redraft_post_warning">Eyða og endurvinna þessa færslu\?</string> + <string name="mute_domain_warning">Ertu alveg algjörlega viss um að þú viljir loka á allt %1$s\? Þú munt ekki sjá efni frá þessu léni í neinum opinberum tímalínum eða í tilkynningunum þínum. Fylgjendur þínir frá þessu léni verða fjarlægðir.</string> + <string name="mute_domain_warning_dialog_ok">Fela allt lénið</string> + <string name="visibility_public">Opinbert: Senda á opinberar tímalínur</string> + <string name="visibility_unlisted">Óskráð: Ekki birt á opinberum tímalínum</string> + <string name="visibility_private">Einungis fylgjendur: Senda einungis á fylgjendur</string> + <string name="visibility_direct">Beint: Senda einungis á notendur sem minnst er á</string> + <string name="pref_title_edit_notification_settings">Tilkynningar</string> + <string name="pref_title_notifications_enabled">Tilkynningar</string> + <string name="pref_title_notification_alerts">Aðvaranir</string> + <string name="pref_title_notification_alert_sound">Aðvara með hljóði</string> + <string name="pref_title_notification_alert_vibrate">Aðvara með titringi</string> + <string name="pref_title_notification_alert_light">Aðvara með ljósi</string> + <string name="pref_title_notification_filters">Láta mig vita þegar</string> + <string name="pref_title_notification_filter_mentions">minnst er á</string> + <string name="pref_title_notification_filter_follows">fylgst er með</string> + <string name="pref_title_notification_filter_reblogs">færslurnar mínar eru endurbirtar</string> + <string name="pref_title_notification_filter_favourites">færslurnar mínar eru settar í eftirlæti</string> + <string name="pref_title_notification_filter_poll">könnunum er lokið</string> + <string name="pref_title_appearance_settings">Útlit</string> + <string name="pref_title_app_theme">Þema forrits</string> + <string name="pref_title_timelines">Tímalínur</string> + <string name="pref_title_timeline_filters">Síur</string> + <string name="app_them_dark">Dökkt</string> + <string name="app_theme_light">Ljóst</string> + <string name="app_theme_black">Svart</string> + <string name="app_theme_auto">Sjálfvirkt við sólarlag</string> + <string name="app_theme_system">Nota kerfishönnun</string> + <string name="pref_title_browser_settings">Vafri</string> + <string name="pref_title_custom_tabs">Nota sérsniðna flipa Chrome</string> + <string name="pref_title_language">Tungumál</string> + <string name="pref_title_bot_overlay">Birta merki á róbótum</string> + <string name="pref_title_animate_gif_avatars">Sýna hreyfingar GIF-auðkennismynda</string> + <string name="pref_title_post_filter">Síun tímalínu</string> + <string name="pref_title_post_tabs">Heimatímalína</string> + <string name="pref_title_show_boosts">Sýna endurbirtingar</string> + <string name="pref_title_show_replies">Sýna svör</string> + <string name="pref_title_show_media_preview">Sækja forskoðanir á myndefni</string> + <string name="pref_title_proxy_settings">Milliþjónn</string> + <string name="pref_title_http_proxy_settings">HTTP-milliþjónn</string> + <string name="pref_title_http_proxy_enable">Virkja HTTP-milliþjón</string> + <string name="pref_title_http_proxy_server">HTTP-milliþjónn (proxy)</string> + <string name="pref_title_http_proxy_port">Gátt HTTP milliþjóns (vefsels)</string> + <string name="pref_default_post_privacy">Sjálfgefin gagnaleynd færslna</string> + <string name="pref_default_media_sensitivity">Alltaf merkja myndefni sem viðkvæmt</string> + <string name="pref_publishing">Gefið út (samstillt við vefþjón)</string> + <string name="pref_failed_to_sync">Mistókst að samstilla kjörstillingar</string> + <string name="post_privacy_public">Opinbert</string> + <string name="post_privacy_unlisted">Óskráð</string> + <string name="post_privacy_followers_only">Einungis fylgjendur</string> + <string name="pref_post_text_size">Textastærð stöðufærslu</string> + <string name="post_text_size_smallest">Minnstu</string> + <string name="post_text_size_small">Lítið</string> + <string name="post_text_size_medium">Miðlungs</string> + <string name="post_text_size_large">Stórt</string> + <string name="post_text_size_largest">Stærst</string> + <string name="notification_mention_name">Nýjar tilvísanir</string> + <string name="notification_mention_descriptions">Tilkynningar um nýjar tilvísanir</string> + <string name="notification_follow_name">Nýir fylgjendur</string> + <string name="notification_follow_description">Tilkynningar um nýja fylgjendur</string> + <string name="notification_boost_name">Endurbirtingar</string> + <string name="notification_boost_description">Tilkynningar þegar færslurnar þínar eru endurbirtar</string> + <string name="notification_favourite_name">Eftirlæti</string> + <string name="notification_favourite_description">Tilkynningar þegar færslurnar þínar eru settar í eftirlæti</string> + <string name="notification_poll_name">Kannanir</string> + <string name="notification_poll_description">Tilkynningar um kannanir sem er lokið</string> + <string name="notification_mention_format">%1$s minntist á þig</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s og %4$d til viðbótar</string> + <string name="notification_summary_medium">%1$s, %2$s og %3$s</string> + <string name="notification_summary_small">%1$s og %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d ný aðgerð</item> + <item quantity="other">%1$d nýjar aðgerðir</item> + </plurals> + <string name="description_account_locked">Læstur notandaaðgangur</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_powered_by_tusky">Keyrir á Tusky</string> + <string name="about_tusky_license">Tusky er frjáls hugbúnaður með opinn grunnkóða. Hann er gefinn út með GNU General Public notkunarleyfi, útgáfu 3. Þú getur skoðað notkunarleyfið hér: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="about_project_site">Vefsvæði verkefnisins: https://tusky.app</string> + <string name="about_bug_feature_request_site">Villutilkynningar og beiðnir um nýja eiginleika: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Notandasnið Tusky</string> + <string name="post_share_content">Deila efni úr færslu</string> + <string name="post_share_link">Deila tengli á færslu</string> + <string name="post_media_images">Myndir</string> + <string name="post_media_video">Myndskeið</string> + <string name="state_follow_requested">Beðið um að fylgja</string> + <string name="abbreviated_in_years">eftir %1$dár</string> + <string name="abbreviated_in_days">eftir %1$dd</string> + <string name="abbreviated_in_hours">eftir %1$dklst</string> + <string name="abbreviated_in_minutes">eftir %1$dm</string> + <string name="abbreviated_in_seconds">eftir %1$ds</string> + <string name="abbreviated_years_ago">%1$dár</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_hours_ago">%1$dklst</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="follows_you">Fylgir þér</string> + <string name="pref_title_alway_show_sensitive_media">Alltaf birta myndefni sem merkt er viðkvæmt</string> + <string name="pref_title_alway_open_spoiler">Alltaf fletta út færslum sem eru með aðvörun vegna efnis</string> + <string name="title_media">Gagnaskrár</string> + <string name="replying_to">Svar til @%1$s</string> + <string name="load_more_placeholder_text">hlaða inn fleiru</string> + <string name="pref_title_public_filter_keywords">Opinberar tímalínur</string> + <string name="pref_title_thread_filter_keywords">Samtöl</string> + <string name="filter_addition_title">Bæta við síu</string> + <string name="filter_edit_title">Breyta síu</string> + <string name="filter_dialog_remove_button">Fjarlægja</string> + <string name="filter_dialog_update_button">Uppfæra</string> + <string name="filter_dialog_whole_word">Heil orð</string> + <string name="filter_dialog_whole_word_description">Þegar stikkorð eða frasi er einungis tölur og bókstafir, verður það aðeins notað ef það samsvarar heilu orði</string> + <string name="filter_add_description">Frasi sem á að sía</string> + <string name="add_account_name">Bæta við aðgang</string> + <string name="add_account_description">Bæta við nýjum Mastodon-aðgangi</string> + <string name="error_create_list">Ekki tókst að búa til lista</string> + <string name="error_rename_list">Ekki tókst að uppfæra lista</string> + <string name="error_delete_list">Ekki tókst að eyða lista</string> + <string name="action_create_list">Búa til lista</string> + <string name="action_rename_list">Uppfæra listann</string> + <string name="action_delete_list">Eyða listanum</string> + <string name="hint_search_people_list">Leita að fólki sem þú fylgist með</string> + <string name="action_add_to_list">Bæta notandaaðgangi á listann</string> + <string name="action_remove_from_list">Fjarlægja notandaaðganginn af listanum</string> + <string name="compose_active_account_description">Sendi sem %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Lýstu þessu fyrir sjónskerta (hámark %1$d stafur)</item> + <item quantity="other">Lýstu þessu fyrir sjónskerta (hámark %1$d stafir)</item> + </plurals> + <string name="action_set_caption">Setja skýringatexta</string> + <string name="action_remove">Fjarlægja</string> + <string name="lock_account_label">Læsa notandaaðgangi</string> + <string name="lock_account_label_description">Krefst þess að þú samþykkir fylgjendur handvirkt</string> + <string name="compose_save_draft">Vista drög\?</string> + <string name="send_post_notification_title">Sendi færslu…</string> + <string name="send_post_notification_error_title">Villa við að senda færslu</string> + <string name="send_post_notification_channel_name">Sendi færslur</string> + <string name="send_post_notification_cancel_title">Aflýsti sendingu</string> + <string name="send_post_notification_saved_content">Afrit af færslunni hefur verið vistað í drögunum þínum</string> + <string name="action_compose_shortcut">Semja skilaboð</string> + <string name="error_no_custom_emojis">Tilvikið þitt %1$s er ekki með nein sérsniðin tjáningartákn</string> + <string name="emoji_style">Stíll tjáningartákna</string> + <string name="system_default">Sjálfgefið í kerfinu</string> + <string name="download_fonts">Þú þarft fyrst að ná í þessi táknmyndasett</string> + <string name="performing_lookup_title">Framkvæmi uppflettingu…</string> + <string name="expand_collapse_all_posts">Þenja út / Fella saman allar stöðufærslur</string> + <string name="action_open_post">Opna færslu</string> + <string name="restart_required">Endurræsing forrits er nauðsynleg</string> + <string name="restart_emoji">Það þarf að endurræsa Tusky til að breytingarnar taki gildi</string> + <string name="later">Seinna</string> + <string name="restart">Endurræsa</string> + <string name="caption_systememoji">Sjálfgefið táknmyndasett á tækinu þnu</string> + <string name="caption_blobmoji">Blob-táknmyndasettið sem þekkt var í Android 4.4–7.1</string> + <string name="caption_twemoji">Staðlaða Mastodon táknmyndasettið</string> + <string name="caption_notoemoji">Núverandi táknmyndasett Google</string> + <string name="download_failed">Niðurhal mistókst</string> + <string name="profile_badge_bot_text">Róbót</string> + <string name="account_moved_description">%1$s hefur verið flutt á:</string> + <string name="reblog_private">Endurbirta til upphaflegra lesenda</string> + <string name="unreblog_private">Taka úr endurbirtingu</string> + <string name="license_description">Tusky inniheldur kóða og gögn frá eftirfarandi verkefnum með opinn grunnkóða:</string> + <string name="license_apache_2">Notkunarleyfi er samkvæmt Apache hugbúnaðarleyfinu (afrit fyrir neðan)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Lýsigögn notandasniðs</string> + <string name="profile_metadata_add">bæta við gögnum</string> + <string name="profile_metadata_label_label">Merking</string> + <string name="profile_metadata_content_label">Efni</string> + <string name="pref_title_absolute_time">Nota algildan tíma</string> + <string name="label_remote_account">Ekki er víst að upplýsingarnar hér að neðan endurspegli notandasniðið að fullu. Opnaðu fullt notandasnið í vafra.</string> + <string name="unpin_action">Losa</string> + <string name="pin_action">Festa</string> + <string name="title_reblogged_by">Endurbirt af</string> + <string name="title_favourited_by">Sett í eftirlæti af</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s og %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s og %3$d til viðbótar</string> + <string name="description_post_media">Myndefni: %1$s</string> + <string name="description_post_cw">Aðvörun vegna efnis: %1$s</string> + <string name="description_post_media_no_description_placeholder">Engin lýsing</string> + <string name="description_post_reblogged">Endurbloggað</string> + <string name="description_post_favourited">Í eftirlætum</string> + <string name="description_post_bookmarked">Bókamerkt</string> + <string name="description_visibility_public">Opinbert</string> + <string name="description_visibility_unlisted">Óskráð</string> + <string name="description_visibility_private">Fylgjendur</string> + <string name="description_visibility_direct">Beint</string> + <string name="description_poll">Könnun með valkostunum: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="hint_list_name">Heiti á lista</string> + <string name="edit_hashtag_hint">Myllumerki án #</string> + <string name="select_list_title">Veldu lista</string> + <string name="list">Listi</string> + <string name="notifications_clear">Eyða</string> + <string name="notifications_apply_filter">Sía</string> + <string name="filter_apply">Virkja</string> + <string name="compose_shortcut_long_label">Semja færslu</string> + <string name="compose_shortcut_short_label">Semja skilaboð</string> + <string name="notification_clear_text">Ertu viss um að þú viljir endanlega eyða öllum tilkynningunum þínum\?</string> + <string name="compose_preview_image_description">Aðgerðir fyrir mynd %1$s</string> + <string name="poll_info_format"> <!-- 15 atkvæði • 1 klukkustund eftir --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s atkvæði</item> + <item quantity="other">%1$s atkvæði</item> + </plurals> + <string name="poll_info_time_absolute">lýkur %1$s</string> + <string name="poll_info_closed">lokað</string> + <string name="poll_vote">Greiða atkvæði</string> + <string name="poll_ended_voted">Könnun sem þú tókst þátt í er lokið</string> + <string name="poll_ended_created">Könnuninni þinni er lokið</string> + <string name="button_continue">Halda áfram</string> + <string name="button_back">Til baka</string> + <string name="button_done">Lokið</string> + <string name="report_sent_success">Tókst að kæra @%1$s</string> + <string name="hint_additional_info">Aðrar athugasemdir</string> + <string name="report_remote_instance">Áframsenda til %1$s</string> + <string name="failed_report">Mistókst að kæra</string> + <string name="failed_fetch_posts">Mistókst að sækja stöðufærslur</string> + <string name="report_description_1">Kæran verður send á umsjónarmenn vefþjónsins þíns. Þú getur gefið skýringu hér fyrir neðan á því af hverju þú ert að kæra þennan notandaaðgang:</string> + <string name="report_description_remote_instance">Notandaaðgangurinn er af öðrum vefþjóni. Á einnig að senda nafnlaust afrit af kærunni þangað\?</string> + <string name="title_accounts">Notandaaðgangar</string> + <string name="failed_search">Tókst ekki að leita</string> + <string name="create_poll_title">Athuga</string> + <string name="duration_5_min">5 mínútur</string> + <string name="duration_30_min">30 mínútur</string> + <string name="duration_1_hour">1 klukkustund</string> + <string name="duration_6_hours">6 klukkustundir</string> + <string name="duration_1_day">1 dagur</string> + <string name="duration_3_days">3 dagar</string> + <string name="duration_7_days">7 dagar</string> + <string name="add_poll_choice">Bæta við valkosti</string> + <string name="poll_allow_multiple_choices">Margir valkostir</string> + <string name="poll_new_choice_hint">Valkostur %1$d</string> + <string name="edit_poll">Breyta</string> + <string name="post_lookup_error_format">Villa við að fletta upp færslunni %1$s</string> + <string name="no_drafts">Þú ert ekki með nein drög.</string> + <string name="no_scheduled_posts">Þú ert ekki með neinar áætlaðar stöðufærslur.</string> + <string name="warning_scheduling_interval">Mastodon er með 5 mínútna lágmarksbil fyrir áætlaðar aðgerðir.</string> + <string name="notification_follow_request_name">Fylgjendabeiðnir</string> + <string name="hashtags">Myllumerki</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> eftirlæti</item> + <item quantity="other"><b>%1$s</b> eftirlæti</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> Endurbirting</item> + <item quantity="other"><b>%1$s</b> Endurbirtingar</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d sekúnda eftir</item> + <item quantity="other">%1$d sekúndur eftir</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d mínúta eftir</item> + <item quantity="other">%1$d mínútur eftir</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d klukkustund eftir</item> + <item quantity="other">%1$d klukkustundir eftir</item> + </plurals> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d dagur eftir</item> + <item quantity="other">%1$d dagar eftir</item> + </plurals> + <string name="account_note_saved">Vistað!</string> + <string name="account_note_hint">Þín eigin einkaathugasemd um þennan aðgang</string> + <string name="pref_title_hide_top_toolbar">Fela titil á verkfærastikunni efst</string> + <string name="pref_title_confirm_reblogs">Birta staðfestingu áður en endurbirting fer fram</string> + <string name="pref_title_show_cards_in_timelines">Birta forskoðun tengla á tímalínum</string> + <string name="no_announcements">Það eru engar tilkynningar.</string> + <string name="pref_title_enable_swipe_for_tabs">Virkja strokuhreyfingu til að skipta milli flipa</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s aðili</item> + <item quantity="other">%1$s aðilar</item> + </plurals> + <string name="add_hashtag_title">Bæta við myllumerki</string> + <string name="notification_follow_request_description">Tilkynningar um fylgjendabeiðnir</string> + <string name="pref_main_nav_position_option_bottom">Neðst</string> + <string name="pref_main_nav_position_option_top">Efst</string> + <string name="pref_main_nav_position">Aðalstaða leiðsagnar</string> + <string name="pref_title_gradient_for_media">Birta litstigla í stað falins myndefnis</string> + <string name="pref_title_notification_filter_follow_requests">beiðni um að fylgja</string> + <string name="dialog_mute_hide_notifications">Fela tilkynningar</string> + <string name="dialog_mute_warning">Þagga niður í @%1$s\?</string> + <string name="dialog_block_warning">Loka á @%1$s\?</string> + <string name="action_unmute_conversation">Hætta að þagga niður í samtali</string> + <string name="action_mute_conversation">Þagga niður í samtali</string> + <string name="action_unmute_domain">Afþagga %1$s</string> + <string name="action_unmute_desc">Afþagga %1$s</string> + <string name="notification_follow_request_format">%1$s bað um að fylgjast með þér</string> + <string name="title_announcements">Tilkynningar</string> + <string name="wellbeing_mode_notice">Sumar upplýsingar sem gætu haft áhrif á andlega vellíðan þína verða faldar. Þetta hefur áhrif á: +\n +\n - Eftirlæti/Endurbirtingar/Tilkynningar um fylgjendabeiðnir +\n - Eftirlæti/Talningu á endurbirtingum færslna +\n - Fylgjendur/Tölfræði færslna í notendasniðum +\n +\n Þetta hefur ekki áhrif á ýti-tilkynningar, en þú getur yfirfarið handvirkt kjörstillingar þínar varðandi tilkynningar.</string> + <string name="action_unsubscribe_account">Segja upp áskrift</string> + <string name="action_subscribe_account">Gerast áskrifandi</string> + <string name="pref_title_animate_custom_emojis">Hreyfa sérsniðin tjáningartákn</string> + <string name="drafts_post_reply_removed">Færslan sem þú gerðir drög að svari við hefur verið fjarlægð</string> + <string name="draft_deleted">Eyddi drögum</string> + <string name="drafts_failed_loading_reply">Mistókst að hlaða inn svarupplýsingum</string> + <string name="drafts_post_failed_to_send">Mistókst að senda þessa færslu!</string> + <string name="post_media_attachments">Viðhengi</string> + <string name="post_media_audio">Hljóð</string> + <string name="dialog_delete_list_warning">Ertu viss um að þú viljir eyða %1$s listanum\?</string> + <string name="duration_indefinite">Ótiltekið</string> + <string name="label_duration">Tímalengd</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Þú getur ekki sent inn fleiri en %1$d myndefnisviðhengi.</item> + <item quantity="other">Þú getur ekki sent inn fleiri en %1$d myndefnisviðhengi.</item> + </plurals> + <string name="wellbeing_hide_stats_profile">Fela magntölfræði notendasniða</string> + <string name="wellbeing_hide_stats_posts">Fela magntölfræði færslna</string> + <string name="limit_notifications">Takmarka tilkynningar á tímalínu</string> + <string name="review_notifications">Yfirfara tilkynningar</string> + <string name="pref_title_wellbeing_mode">Vellíðan</string> + <string name="notification_subscription_description">Tilkynningar þegar einhver sem þú ert áskrifandi að hefur birt nýja færslu</string> + <string name="notification_subscription_name">Nýjar færslur</string> + <string name="pref_title_notification_filter_subscriptions">einhver sem ég er áskrifandi að birti nýja færslu</string> + <string name="notification_subscription_format">%1$s sendi inn rétt í þessu</string> + <string name="follow_requests_info">Jafnvel þótt aðgangurinn þinn sé ekki læstur, fannst starfsfólki %1$s að þú gætir viljað yfirfara handvirkt fylgjendabeiðnir frá þessum aðgöngum.</string> + <string name="action_unbookmark">Fjarlægja bókamerki</string> + <string name="pref_title_confirm_favourites">Birta staðfestingu áður en sett er í eftirlæti</string> + <string name="dialog_delete_conversation_warning">Eyða þessu samtali\?</string> + <string name="action_delete_conversation">Eyða samtali</string> + <string name="duration_30_days">30 dagar</string> + <string name="duration_60_days">60 dagar</string> + <string name="duration_90_days">90 dagar</string> + <string name="duration_180_days">180 dagar</string> + <string name="duration_365_days">365 dagar</string> + <string name="duration_14_days">14 dagar</string> + <string name="tusky_compose_post_quicksetting_label">Semja færslu</string> + <string name="notification_sign_up_format">%1$s skráði sig</string> + <string name="pref_title_notification_filter_sign_ups">einhver skráði sig</string> + <string name="notification_update_format">%1$s breytti færslunni sinni</string> + <string name="pref_title_notification_filter_updates">færsla sem ég hef átt við er breytt</string> + <string name="notification_sign_up_name">Nýskráningar</string> + <string name="notification_sign_up_description">Tilkynningar um nýja notendur</string> + <string name="notification_update_name">Breytingar á færslum</string> + <string name="notification_update_description">Tilkynningar þegar færslum sem þú hefur átt við er breytt</string> + <string name="dialog_push_notification_migration_other_accounts">Þú hefur skráð þig aftur inn í fyrirliggjandi aðganginn þinn til þess að veita heimild fyrir áskrift að ýti-tilkynningum í Tusky. Aftur á móti ertu með aðra aðganga sem ekki hafa verið yfirfærðir á þennan hátt. Skiptu yfir í þá og skráðu þig þar inn aftur til að virkja stuðning við tilkynningar í gegnum UnifiedPush.</string> + <string name="account_date_joined">Skráði sig %1$s</string> + <string name="tips_push_notification_migration">Skrá aftur inn alla aðganga til að virkja stuðning við ýti-tilkynningar.</string> + <string name="dialog_push_notification_migration">Til þess að geta sent ýti-tilkynningar í gegnum UnifiedPush, þarf Tusky heimild til að gerast áskrifandi að tilkynningum á Mastodon-netþjóninum þínum. Þetta krefst þess að skráð sé inn aftur til að breyta vægi OAuth-heimilda sem Tusky er úthlutað. Notaðu endurinnskráninguna hérna eða í kjörstillingum aðgangsins þíns til að varðveita öll drögin þín og skyndiminni á tækinu.</string> + <string name="title_login">Skrá inn</string> + <string name="status_count_one_plus">1+</string> + <string name="error_could_not_load_login_page">Gat ekki lesið innskráningarsíðuna.</string> + <string name="action_edit_image">Breyta mynd</string> + <string name="saving_draft">Vista drög…</string> + <string name="title_migration_relogin">Skráðu aftur inn fyrir ýti-tilkynningar</string> + <string name="action_dismiss">Hunsa</string> + <string name="action_details">Nánar</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="error_multimedia_size_limit">Myndskeiða- og hljóðskrár geta ekki verið stærri en %1$s MB.</string> + <string name="description_post_language">Tungumál færslu</string> + <string name="duration_no_change">(engin breyting)</string> + <string name="error_following_hashtag_format">Villa við að fylgjast með #%1$s</string> + <string name="error_unfollowing_hashtag_format">Villa við að hætta að fylgjast með #%1$s</string> + <string name="error_loading_account_details">Mistókst að hlaða inn nánari upplýsingum notandaaðgangs</string> + <string name="error_image_edit_failed">Ekki var hægt að breyta myndinni.</string> + <string name="delete_scheduled_post_warning">Eyða þessari áætluðu færslu\?</string> + <string name="instance_rule_title">Reglur %1$s</string> + <string name="instance_rule_info">Með því að skrá þig inn samþykkir þú reglurnar á %1$s.</string> + <string name="action_add_reaction">bæta við viðbrögðum</string> + <string name="set_focus_description">Ýttu eða dragðu hringinn til að setja virknistað sem verður ævinlega sýnilegur í smámyndum.</string> + <string name="compose_save_draft_loses_media">Vista drög\? (Viðhengi verða send inn aftur þegar þú endurheimtir drögin.)</string> + <string name="failed_to_pin">Mistókst að festa</string> + <string name="failed_to_unpin">Mistókst að losa</string> + <string name="pref_show_self_username_always">Alltaf</string> + <string name="pref_show_self_username_disambiguate">Þegar er skráð inn á mörgum aðgöngum</string> + <string name="action_set_focus">Setja virknistað</string> + <string name="pref_show_self_username_never">Aldrei</string> + <string name="pref_title_show_self_username">Birta notandanafn á verkfærastikum</string> + <string name="action_add_or_remove_from_list">Bæta við eða fjarlægja af lista</string> + <string name="failed_to_add_to_list">Mistókst að bæta notandaaðgangnum á listann</string> + <string name="failed_to_remove_from_list">Mistókst að fjarlægja notandaaðganginn af listanum</string> + <string name="no_lists">Þú ert ekki með neina lista.</string> + <string name="pref_default_post_language">Sjálfgefið tungumál færslna</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="error_following_hashtags_unsupported">Þessi netþjónn styður ekki að fylgst sé með myllumerkjum.</string> + <string name="title_followed_hashtags">Myllumerki sem fylgst er með</string> + <string name="status_created_at_now">núna</string> + <string name="action_unfollow_hashtag_format">Hætta að fylgjast með #%1$s\?</string> + <string name="notification_report_format">Ný kæra vegna %1$s</string> + <string name="notification_header_report_format">%1$s kærði %2$s</string> + <string name="notification_summary_report_format">%1$s · %2$d færslur viðhengdar</string> + <string name="post_edited">Breytti %1$s</string> + <string name="confirmation_hashtag_unfollowed">Hætt að fylgjast með #%1$s</string> + <string name="notification_report_name">Kærur</string> + <string name="notification_report_description">Tilkynningar um kærur umsjónarmanna</string> + <string name="pref_title_notification_filter_reports">það er ný kæra</string> + <string name="description_post_edited">Breytti</string> + <string name="report_category_violation">Brot á reglu</string> + <string name="report_category_spam">Ruslpóstur</string> + <string name="report_category_other">Annað</string> + <string name="error_muting_hashtag_format">Villa við að þagga niður í #%1$s</string> + <string name="error_unmuting_hashtag_format">Villa við að hætta að þagga niður í #%1$s</string> + <string name="hint_media_description_missing">Myndefni ætti að vera með lýsingu.</string> + <string name="pref_title_http_proxy_port_message">Gáttin ætti að vera á milli %1$d og %2$d</string> + <string name="error_status_source_load">Mistókst að hlaða inn uppruna stöðufærslu af netþjóninum.</string> + <string name="a11y_label_loading_thread">Hleð inn þræði</string> + <string name="pref_title_reading_order">Lestrarröð</string> + <string name="pref_reading_order_oldest_first">Elsta fyrst</string> + <string name="pref_reading_order_newest_first">Nýjasta fyrst</string> + <string name="compose_unsaved_changes">Þú ert með óvistaðar breytingar.</string> + <string name="pref_summary_http_proxy_disabled">Óvirkt</string> + <string name="pref_summary_http_proxy_missing"><ekki stillt></string> + <string name="pref_summary_http_proxy_invalid"><ógilt></string> + <string name="title_edits">Breytingar</string> + <string name="status_created_info">%1$s bjó til</string> + <string name="status_edit_info">%1$s breytti</string> + <string name="post_media_alt">TEXTI</string> + <string name="action_discard">Henda breytingum</string> + <string name="action_continue_edit">Halda breytingum áfram</string> + <string name="action_share_account_link">Deila tengli á notandaaðgang</string> + <string name="action_share_account_username">Deila notandanafni aðgangs</string> + <string name="send_account_link_to">Deila slóð aðgangs til…</string> + <string name="send_account_username_to">Deila notandanafni aðgangs til…</string> + <string name="account_username_copied">Notandanafn afritað</string> + <string name="mute_notifications_switch">Þagga tilkynningar</string> + <string name="title_public_trending_hashtags">Vinsæl myllumerki</string> + <string name="accessibility_talking_about_tag">%1$d manns eru að tala um myllumerkið %2$s</string> + <string name="action_post_failed">Innsending mistókst</string> + <string name="action_post_failed_show_drafts">Birta drög</string> + <string name="action_post_failed_do_nothing">Afgreiða</string> + <string name="action_post_failed_detail">Ekki tókst að senda inn færsluna þína og hefur hún verið vistuð í Drög. +\n +\nAnnað hvort náðist ekki í netþjóninn, eða hann hafnaði færslunni.</string> + <string name="action_post_failed_detail_plural">Ekki tókst að senda inn færslurnar þína og hafa þær verið vistaðar í Drög. +\n +\nAnnað hvort náðist ekki í netþjóninn, eða hann hafnaði færslunum.</string> + <string name="action_browser_login">Skrá inn í vafra</string> + <string name="description_login">Virkar í flestum tilvikum. Engum gögnum er lekið til annarra forrita.</string> + <string name="description_browser_login">Gæti stutt fleiri aðferðir til auðkenningar, en krefst studds vafra.</string> + <string name="total_usage">Heildarnotkun</string> + <string name="total_accounts">Aðgangar alls</string> + <string name="dialog_follow_hashtag_title">Fylgjast með myllumerki</string> + <string name="dialog_follow_hashtag_hint">#myllumerki</string> + <string name="notification_unknown_name">Óþekkt</string> + <string name="action_refresh">Endurlesa</string> + <string name="status_filtered_show_anyway">Birta samt</string> + <string name="status_filter_placeholder_label_format">Síað: %1$s</string> + <string name="pref_title_account_filter_keywords">Notendasnið</string> + <string name="pref_title_show_stat_inline">Sýna tölfræði færslu í tímalínu</string> + <string name="ui_error_favourite">Mistókst að setja færslu í eftirlæti: %1$s</string> + <string name="ui_error_reblog">Mistókst að endurbirta færslu: %1$s</string> + <string name="ui_error_vote">Mistókst að greiða atkvæði í könnun: %1$s</string> + <string name="ui_error_accept_follow_request">Mistókst að samþykkja fylgjendabeiðni: %1$s</string> + <string name="hint_filter_title">Sían mín</string> + <string name="label_filter_title">Titill</string> + <string name="filter_action_warn">Aðvörun</string> + <string name="filter_action_hide">Fela</string> + <string name="filter_description_warn">Fela með aðvörun</string> + <string name="filter_description_hide">Fela alveg</string> + <string name="ui_error_bookmark">Mistókst að bókamerkja færslu: %1$s</string> + <string name="socket_timeout_exception">Rann út á tíma við að tengjast netþjóninum þínum</string> + <string name="ui_error_unknown">óþekkt ástæða</string> + <string name="ui_error_clear_notifications">Vandamál með hreinsun á tilkynningum: %1$s</string> + <string name="ui_error_reject_follow_request">Mistókst að hafna fylgjendabeiðni: %1$s</string> + <string name="ui_success_accepted_follow_request">Samþykkt beiðni um að fylgjast með</string> + <string name="ui_success_rejected_follow_request">Lokað á beiðni um að fylgjast með</string> + <string name="label_filter_action">Aðgerð síu</string> + <string name="label_filter_context">Samhengi síu</string> + <string name="label_filter_keywords">Stikkorð eða setningar sem á að sía</string> + <string name="action_add">Bæta við</string> + <string name="filter_keyword_display_format">%1$s (heilt orð)</string> + <string name="filter_keyword_addition_title">Bæta við stikkorði</string> + <string name="filter_edit_keyword_title">Breyta stikkorði</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="post_media_image">Mynd</string> + <string name="select_list_manage">Sýsla með lista</string> + <string name="help_empty_home">Þetta er <b>tímalínan þín</b>. Hún sýnir nýlegar færslur þeirra sem þú fylgist með. +\n +\nTil að skoða hvað aðrir eru að gera getur þú til dæmis uppgötvað viðkomandi í einni af hinum tímalínunum. Til dæmis á staðværu tímalínu netþjónsins þíns [iconics gmd_group]. Eða að þú leitar að þeim eftir nafni [iconics gmd_search]; til dæmis geturðu leitað að Tusky til að finna Mastodon-aðganginn okkar.</string> + <string name="pref_ui_text_size">Textastærð viðmóts</string> + <string name="notification_listenable_worker_name">Bakgrunnsvirkni</string> + <string name="notification_listenable_worker_description">Tilkynningar þegar Tusky er að vinna í bakgrunni</string> + <string name="notification_notification_worker">Sæki tilkynningar…</string> + <string name="notification_prune_cache">Viðhaldsvinna biðminnis…</string> + <string name="load_newest_notifications">Hlaða inn nýjustu tilkynningum</string> + <string name="compose_delete_draft">Eyða drögum\?</string> + <string name="error_missing_edits">Þjónninn þinn veit að þessari færslu hefur verið breytt, en er hins vegar ekki með afrit af breytingunum, þannig að ekki er hægt að sýna þér þær. +\n +\nÞetta er <a href="https://github.com/mastodon/mastodon/issues/25398">Mastodon verkbeiðni #25398</a>.</string> + <string name="about_device_info_title">Tækið þitt</string> + <string name="about_device_info">%1$s %2$s +\nAndroid-útgáfa: %3$s +\nSDK-útgáfa: %4$d</string> + <string name="about_account_info_title">Notandaaðgangurinn þinn</string> + <string name="about_account_info">\@%1$s@%2$s +\nÚtgáfa: %3$s</string> + <string name="about_copy">Afrita upplýsingar um útgáfu og tæki</string> + <string name="about_copied">Afritaði upplýsingar um útgáfu og tæki</string> + <string name="list_exclusive_label">Fela frá tímalínu</string> + <string name="error_media_playback">Afspilun mistókst: %1$s</string> + <string name="dialog_delete_filter_text">Eyða síunni \'%1$s\'\?</string> + <string name="dialog_delete_filter_positive_action">Eyða</string> + <string name="muting_hashtag_success_format">Þagga niður í myllumerkinu #%1$s sem aðvörun</string> + <string name="unmuting_hashtag_success_format">Hætti að þagga niður í myllumerkinu #%1$s</string> + <string name="action_view_filter">Skoða síu</string> + <string name="following_hashtag_success_format">Fylgist núna með myllumerkinu #%1$s</string> + <string name="unfollowing_hashtag_success_format">Fylgist ekki lengur með myllumerkinu #%1$s</string> + <string name="dialog_save_profile_changes_message">Viltu vista breytingarnar á notandasniðinu þínu\?</string> + <string name="error_blocking_domain">Mistókst að þagga niður í %1$s: %2$s</string> + <string name="error_unblocking_domain">Mistókst að hætta að þagga niður í %1$s: %2$s</string> + <string name="error_media_upload_sending_fmt">Innsendingin mistókst: %1$s</string> + <string name="help_empty_conversations">Hér eru <b>einkaskilaboðin</b> þín; stundum kölluð samtöl eða bein skilaboð (DM). +\n +\nEinkaskilaboð eru gerð með því að stilla sýnileika [iconics gmd_public] færslu [iconics gmd_mail] á <i>Beint</i> og að minnast á einn eða fleiri notendur í textanum. +\n +\nÞú getur til dæmis farið á notandaaðgang einhvers og ýtt á \'Minnast á\'-hnappinn [iconics gmd_edit] og breytt sýnileikanum. </string> + <string name="help_empty_lists">Þetta er <b>listasýnin</b> þín. Þú getur skilgreint fjölmarga einkalista og bætt notendaaðgöngum á þá. +\n +\n ATHUGAÐU: þú getur bara bætt notendaaðgöngum sem þú fylgist með á listana þína. +\n +\n Þessa lista má nota sem flipa í flipum [iconics gmd_account_circle] [iconics gmd_navigate_next] Kjörstillingar aðgangs. </string> + <string name="label_image">Mynd</string> + <string name="app_theme_system_black">Nota hönnun kerfis (svart)</string> + <string name="title_public_trending_statuses">Færslur í umræðunni</string> + <string name="list_reply_policy_label">Birta svör til</string> + <string name="list_reply_policy_none">Engra</string> + <string name="list_reply_policy_list">Meðlima listans</string> + <string name="list_reply_policy_followed">Hvers þess sem fylgst er með</string> + <string name="pref_title_show_self_boosts">Sýna sjálfs-endurbirtingar</string> + <string name="pref_title_show_self_boosts_description">Einhver endurbirtir sína eigin færslu</string> + <string name="pref_title_per_timeline_preferences">Kjörstillingar á hverja tímalínu</string> + <string name="reply_sending_long">Verið er að senda svarið þitt.</string> + <string name="reply_sending">Sendi…</string> + <string name="pref_title_show_notifications_filter">Birta tilkynningasíu</string> + <string name="action_translate">Þýða</string> + <string name="action_show_original">Sýna upprunalegt</string> + <string name="label_translating">Þýði…</string> + <string name="label_translated">Þýtt úr %1$s með %2$s</string> + <string name="ui_error_translate">Tókst ekki að þýða: %1$s</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml new file mode 100644 index 0000000..e64a13a --- /dev/null +++ b/app/src/main/res/values-it/strings.xml @@ -0,0 +1,738 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Si è verificato un errore.</string> + <string name="error_network">Si è verificato un errore di rete. Per favore controlla la tua connessione e riprova.</string> + <string name="error_empty">Questo non può essere vuoto.</string> + <string name="error_invalid_domain">Inserito un dominio non valido</string> + <string name="error_failed_app_registration">Autenticazione con quell\'istanza fallita. Se il problema persiste, prova a collegarti dal browser nel menù.</string> + <string name="error_no_web_browser_found">Nessun browser web utilizzabile trovato.</string> + <string name="error_authorization_unknown">Sì è verificato un errore di autenticazione non identificato. Se il problema persiste, prova a collegarti dal browser nel menù.</string> + <string name="error_authorization_denied">Autorizzazione negata. Se sei sicuro di aver usato le credenziali corrette, prova a collegarti con il browser nel menu.</string> + <string name="error_retrieving_oauth_token">Acquisizione token di accesso fallita. Se il problema persiste, prova a collegarti dal browser nel menu.</string> + <string name="error_compose_character_limit">Il post è troppo lungo!</string> + <string name="error_media_upload_type">Quel tipo di file non può essere caricato.</string> + <string name="error_media_upload_opening">Non è stato possibile aprire quel file.</string> + <string name="error_media_upload_permission">È richiesto il permesso di leggere file.</string> + <string name="error_media_download_permission">È richiesto il permesso di salvare file.</string> + <string name="error_media_upload_image_or_video">Non è possibile allegare nello stesso post immagini e video.</string> + <string name="error_media_upload_sending">Il caricamento è fallito.</string> + <string name="error_sender_account_gone">Errore nell\'invio del post.</string> + <string name="title_home">Home</string> + <string name="title_notifications">Notifiche</string> + <string name="title_public_local">Locale</string> + <string name="title_public_federated">Federata</string> + <string name="title_direct_messages">Messaggi diretti</string> + <string name="title_tab_preferences">Schede</string> + <string name="title_view_thread">Conversazione</string> + <string name="title_posts">Post</string> + <string name="title_posts_with_replies">Con risposte</string> + <string name="title_posts_pinned">Fissati</string> + <string name="title_follows">Seguiti</string> + <string name="title_followers">Seguaci</string> + <string name="title_favourites">Preferiti</string> + <string name="title_mutes">Utenti silenziati</string> + <string name="title_blocks">Utenti bloccati</string> + <string name="title_follow_requests">Richieste di seguirti</string> + <string name="title_edit_profile">Modifica il tuo profilo</string> + <string name="title_drafts">Bozze</string> + <string name="title_licenses">Licenze</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s ha condiviso</string> + <string name="post_sensitive_media_title">Contenuto sensibile</string> + <string name="post_media_hidden_title">Media nascosto</string> + <string name="post_sensitive_media_directions">Clicca per visualizzare</string> + <string name="post_content_warning_show_more">Mostra di più</string> + <string name="post_content_warning_show_less">Mostra di meno</string> + <string name="post_content_show_more">Espandi</string> + <string name="post_content_show_less">Riduci</string> + <string name="message_empty">Qui non c\'è nulla.</string> + <string name="footer_empty">Qui non c\'è nulla. Trascina verso il basso per aggiornare!</string> + <string name="notification_reblog_format">%1$s ha condiviso il tuo post</string> + <string name="notification_favourite_format">%1$s ha messo il tuo messaggio nei preferiti</string> + <string name="notification_follow_format">%1$s ti segue</string> + <string name="report_username_format">Segnala @%1$s</string> + <string name="report_comment_hint">Commenti aggiuntivi?</string> + <string name="action_quick_reply">Risposta veloce</string> + <string name="action_reply">Rispondi</string> + <string name="action_reblog">Condividi</string> + <string name="action_unreblog">Rimuovi condivisione</string> + <string name="action_favourite">Aggiungi ai preferiti</string> + <string name="action_unfavourite">Rimuovi preferito</string> + <string name="action_more">Di più</string> + <string name="action_compose">Componi</string> + <string name="action_login">Accedi con Tusky</string> + <string name="action_logout">Disconnettiti</string> + <string name="action_logout_confirm">Sei sicuro di volerti disconnettere dall\'account %1$s\? Questo cancellerà tutti i dati locali dell\'account, incluse bozze e preferenze.</string> + <string name="action_follow">Segui</string> + <string name="action_unfollow">Smetti di seguire</string> + <string name="action_block">Blocca</string> + <string name="action_unblock">Sblocca</string> + <string name="action_hide_reblogs">Nascondi ricondivisioni</string> + <string name="action_show_reblogs">Mostra ricondivisioni</string> + <string name="action_report">Segnala</string> + <string name="action_delete">Elimina</string> + <string name="action_send">Pubblica</string> + <string name="action_send_public">Pubblica!</string> + <string name="action_retry">Riprova</string> + <string name="action_close">Chiudi</string> + <string name="action_view_profile">Profilo</string> + <string name="action_view_preferences">Preferenze</string> + <string name="action_view_account_preferences">Preferenze account</string> + <string name="action_view_favourites">Preferiti</string> + <string name="action_view_mutes">Utenti silenziati</string> + <string name="action_view_blocks">Utenti bloccati</string> + <string name="action_view_follow_requests">Richieste di seguirti</string> + <string name="action_view_media">Media</string> + <string name="action_open_in_web">Apri nel browser</string> + <string name="action_add_media">Aggiungi media</string> + <string name="action_photo_take">Scatta foto</string> + <string name="action_share">Condividi</string> + <string name="action_mute">Silenzia</string> + <string name="action_unmute">Smetti di silenziare</string> + <string name="action_mention">Menzione</string> + <string name="action_hide_media">Nascondi media</string> + <string name="action_open_drawer">Apri compositore</string> + <string name="action_save">Salva</string> + <string name="action_edit_profile">Modifica profilo</string> + <string name="action_edit_own_profile">Modifica</string> + <string name="action_undo">Annulla</string> + <string name="action_accept">Accetta</string> + <string name="action_reject">Rifiuta</string> + <string name="action_search">Cerca</string> + <string name="action_access_drafts">Bozze</string> + <string name="action_toggle_visibility">Visibilità dei post</string> + <string name="action_content_warning">Avviso di contenuto sensibile</string> + <string name="action_emoji_keyboard">Tastiera emoji</string> + <string name="action_add_tab">Aggiungi scheda</string> + <string name="action_links">Collegamenti</string> + <string name="action_mentions">Menzioni</string> + <string name="action_hashtags">Hashtag</string> + <string name="action_open_reblogger">Vai all\'autore della condivisione</string> + <string name="action_open_reblogged_by">Mostra ricondivisioni</string> + <string name="action_open_faved_by">Mostra preferiti</string> + <string name="title_hashtags_dialog">Hashtag</string> + <string name="title_mentions_dialog">Menzioni</string> + <string name="title_links_dialog">Collegamenti</string> + <string name="action_open_media_n">Apri media #%1$d</string> + <string name="download_image">Scaricando %1$s</string> + <string name="action_copy_link">Copia collegamento</string> + <string name="action_open_as">Apri come %1$s</string> + <string name="action_share_as">Condividi come …</string> + <string name="send_post_link_to">Condividi URL del post su…</string> + <string name="send_post_content_to">Condividi post su…</string> + <string name="send_media_to">Condividi media su…</string> + <string name="confirmation_reported">Inviato!</string> + <string name="confirmation_unblocked">Utente sbloccato</string> + <string name="confirmation_unmuted">Utente non più silenziato</string> + <string name="hint_domain">Quale istanza?</string> + <string name="hint_compose">Cosa succede?</string> + <string name="hint_content_warning">Avviso di contenuto sensibile</string> + <string name="hint_display_name">Mostra nome</string> + <string name="hint_note">Biografia</string> + <string name="hint_search">Cerca…</string> + <string name="search_no_results">Nessun risultato</string> + <string name="label_quick_reply">Rispondi…</string> + <string name="label_avatar">Avatar</string> + <string name="label_header">Intestazione</string> + <string name="link_whats_an_instance">Cos\'è un\'istanza?</string> + <string name="login_connection">Connessione…</string> + <string name="dialog_whats_an_instance">L\'indirizzo o il dominio di qualsiasi istanza può essere inserito qui, come mastodon.social, icosahedron.website, social.tchncs.de, e <a href="https://instances.social">altro!</a> +\n +\nSe non hai ancora un account, puoi inserire il nome di un\'istanza alla quale vuoi iscriverti e creare un account. +\n +\nUn\'istanza è il luogo dove l\'account è custodito, ma puoi facilmente comunicare e seguire gente su altre istanze come se fossero sullo stesso sito. +\n +\nPiù info possono essere trovate su <a href="https://joinmastodon.org">joinmastodon.org</a>. \u0020</string> + <string name="dialog_title_finishing_media_upload">Terminando il caricamento dei media</string> + <string name="dialog_message_uploading_media">Caricamento…</string> + <string name="dialog_download_image">Scarica</string> + <string name="dialog_message_cancel_follow_request">Revocare la richiesta di seguire?</string> + <string name="dialog_unfollow_warning">Smettere di seguire questo account?</string> + <string name="dialog_delete_post_warning">Eliminare questo post\?</string> + <string name="visibility_public">Pubblico: visibile sulle timeline pubbliche</string> + <string name="visibility_unlisted">Non in elenco: non visibile sulle timeline pubbliche</string> + <string name="visibility_private">Solo chi ti segue: visibile solo da chi ti segue</string> + <string name="visibility_direct">Diretto: visibile solo agli utenti menzionati</string> + <string name="pref_title_edit_notification_settings">Notifiche</string> + <string name="pref_title_notifications_enabled">Notifiche</string> + <string name="pref_title_notification_alerts">Allarmi</string> + <string name="pref_title_notification_alert_sound">Notifica con suoneria</string> + <string name="pref_title_notification_alert_vibrate">Notifica con vibrazione</string> + <string name="pref_title_notification_alert_light">Notifica con luce</string> + <string name="pref_title_notification_filters">Notificami quando</string> + <string name="pref_title_notification_filter_mentions">vengo menzionato</string> + <string name="pref_title_notification_filter_follows">vengo seguito</string> + <string name="pref_title_notification_filter_reblogs">i miei post vengono condivisi</string> + <string name="pref_title_notification_filter_favourites">i miei post vengono messi nei preferiti</string> + <string name="pref_title_appearance_settings">Aspetto</string> + <string name="pref_title_app_theme">Tema dell\'app</string> + <string name="pref_title_timelines">Timeline</string> + <string name="pref_title_timeline_filters">Filtri</string> + <string name="app_them_dark">Scuro</string> + <string name="app_theme_light">Chiaro</string> + <string name="app_theme_black">Nero</string> + <string name="app_theme_auto">Automatico al tramonto</string> + <string name="app_theme_system">Usa tema di sistema</string> + <string name="pref_title_browser_settings">Browser</string> + <string name="pref_title_custom_tabs">Usa Custom Tabs di Chrome</string> + <string name="pref_title_language">Lingua</string> + <string name="pref_title_post_filter">Filtraggio della timeline</string> + <string name="pref_title_post_tabs">Timeline principale</string> + <string name="pref_title_show_boosts">Mostra ricondivisioni</string> + <string name="pref_title_show_replies">Mostra risposte</string> + <string name="pref_title_show_media_preview">Mostra anteprime media</string> + <string name="pref_title_proxy_settings">Proxy</string> + <string name="pref_title_http_proxy_settings">Proxy HTTP</string> + <string name="pref_title_http_proxy_enable">Abilita proxy HTTP</string> + <string name="pref_title_http_proxy_server">Server proxy HTTP</string> + <string name="pref_title_http_proxy_port">Porta proxy HTTP</string> + <string name="pref_default_post_privacy">Privacy predefinita dei post (sincronizzato sul server)</string> + <string name="pref_default_media_sensitivity">Segna sempre media come sensibili (sincronizzato sul server)</string> + <string name="pref_publishing">Pubblicazione</string> + <string name="pref_failed_to_sync">Sincronizzazione preferenze fallita</string> + <string name="post_privacy_public">Pubblico</string> + <string name="post_privacy_unlisted">Non in elenco</string> + <string name="post_privacy_followers_only">Solo seguaci</string> + <string name="pref_post_text_size">Dimensione del testo dei post</string> + <string name="post_text_size_smallest">Piccolissimo</string> + <string name="post_text_size_small">Piccolo</string> + <string name="post_text_size_medium">Normale</string> + <string name="post_text_size_large">Grande</string> + <string name="post_text_size_largest">Grandissimo</string> + <string name="notification_mention_name">Nuove menzioni</string> + <string name="notification_mention_descriptions">Notifiche di quando vieni menzionato da qualcuno</string> + <string name="notification_follow_name">Nuovi seguaci</string> + <string name="notification_follow_description">Notifiche su nuovi seguaci</string> + <string name="notification_boost_name">Ricondivisioni</string> + <string name="notification_boost_description">Notifiche sui tuoi post che vengono condivisi</string> + <string name="notification_favourite_name">Preferiti</string> + <string name="notification_favourite_description">Notifiche quando i tuoi post vengono segnati come preferiti</string> + <string name="notification_mention_format">%1$s ti ha menzionato</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s e %4$d altri</string> + <string name="notification_summary_medium">%1$s, %2$s e %3$s</string> + <string name="notification_summary_small">%1$s e %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d nuova interazione</item> + <item quantity="many">%1$d nuove interazioni</item> + <item quantity="other">%1$d nuove interazioni</item> + </plurals> + <string name="description_account_locked">Account bloccato</string> + <string name="about_title_activity">A proposito</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tusky è un programma libero ed open source. + È distribuito con licenza GNU General Public License Version 3. + Puoi leggere la licenza qui: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">Sito web del progetto: https://tusky.app</string> + <string name="about_bug_feature_request_site">Segnala problemi e richiedi funzionalità: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Profilo di Tusky</string> + <string name="post_share_content">Condividi contenuto del post</string> + <string name="post_share_link">Condividi collegamento al post</string> + <string name="post_media_images">Immagini</string> + <string name="post_media_video">Video</string> + <string name="state_follow_requested">Richiesta inviata</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">in %1$d a</string> + <string name="abbreviated_in_days">in %1$dg</string> + <string name="abbreviated_in_hours">in %1$do</string> + <string name="abbreviated_in_minutes">in %1$d min</string> + <string name="abbreviated_in_seconds">in %1$ds</string> + <string name="abbreviated_years_ago">%1$da</string> + <string name="abbreviated_days_ago">%1$dg</string> + <string name="abbreviated_hours_ago">%1$do</string> + <string name="abbreviated_minutes_ago">%1$dmin</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="follows_you">Ti segue</string> + <string name="pref_title_alway_show_sensitive_media">Mostra sempre i contenuti sensibili</string> + <string name="title_media">Media</string> + <string name="replying_to">Rispondendo a @%1$s</string> + <string name="load_more_placeholder_text">carica altro</string> + <string name="pref_title_public_filter_keywords">Timeline pubbliche</string> + <string name="pref_title_thread_filter_keywords">Conversazioni</string> + <string name="filter_addition_title">Aggiungi filtro</string> + <string name="filter_edit_title">Modifica filtro</string> + <string name="filter_dialog_remove_button">Rimuovi</string> + <string name="filter_dialog_update_button">Aggiorna</string> + <string name="filter_add_description">Frase da filtrare</string> + <string name="add_account_name">Aggiungi account</string> + <string name="add_account_description">Aggiungi un nuovo Account Mastodon</string> + <string name="action_lists">Liste</string> + <string name="title_lists">Liste</string> + <string name="error_create_list">Non è stato possibile creare la lista</string> + <string name="error_rename_list">Non è stato possibile aggiornare la lista</string> + <string name="error_delete_list">Non è stato possibile eliminare la lista</string> + <string name="action_create_list">Crea una lista</string> + <string name="action_rename_list">Aggiorna la lista</string> + <string name="action_delete_list">Elimina la lista</string> + <string name="hint_search_people_list">Cerca tra le persone che segui</string> + <string name="action_add_to_list">Aggiungi un account alla lista</string> + <string name="action_remove_from_list">Rimuovi un account dalla lista</string> + <string name="compose_active_account_description">Pubblicando come %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Descrivi i contenuti per gli ipovedenti (limite di %1$d carattere)</item> + <item quantity="many">Descrivi i contenuti per gli ipovedenti (limite di %1$d caratteri)</item> + <item quantity="other">Descrivi i contenuti per gli ipovedenti (limite di %1$d caratteri)</item> + </plurals> + <string name="action_set_caption">Inserisci descrizione</string> + <string name="action_remove">Rimuovi</string> + <string name="lock_account_label">Blocca account</string> + <string name="lock_account_label_description">Richiedi una tua approvazione manuale per seguirti</string> + <string name="compose_save_draft">Salvare bozza?</string> + <string name="send_post_notification_title">Inviando il post…</string> + <string name="send_post_notification_error_title">Errore durante l\'invio</string> + <string name="send_post_notification_channel_name">Invio post</string> + <string name="send_post_notification_cancel_title">Invio annullato</string> + <string name="send_post_notification_saved_content">Una copia del post è stata salvata nelle tue bozze</string> + <string name="action_compose_shortcut">Componi</string> + <string name="error_no_custom_emojis">La tua istanza %1$s non ha nessuna emoji personalizzata</string> + <string name="emoji_style">Stile delle emoji</string> + <string name="system_default">Predefinite del sistema</string> + <string name="download_fonts">Dovrai prima scaricare questo pacchetto di emoji</string> + <string name="performing_lookup_title">Ricerca in corso…</string> + <string name="expand_collapse_all_posts">Espandi/riduci tutti i post</string> + <string name="action_open_post">Apri post</string> + <string name="restart_required">Riavvio dell\'app richiesto</string> + <string name="restart_emoji">Devi riavviare Tusky per applicare queste modifiche</string> + <string name="later">Più tardi</string> + <string name="restart">Riavvia</string> + <string name="caption_systememoji">Le emoji predefinite del tuo dispositivo</string> + <string name="caption_blobmoji">Le emoji Blob di Android 4.4-7.1</string> + <string name="caption_twemoji">Le emoji standard di Mastodon</string> + <string name="download_failed">Scaricamento fallito</string> + <string name="profile_badge_bot_text">Bot</string> + <string name="account_moved_description">%1$s si è spostato su:</string> + <string name="reblog_private">Condividi con la visibilità originale</string> + <string name="unreblog_private">Annulla condivisione</string> + <string name="license_description">Tusky contiene codice e risorse dai seguenti progetti open source:</string> + <string name="license_apache_2">Licenziata sotto la Licenza Apache (copia sotto)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Metadati del profilo</string> + <string name="profile_metadata_add">aggiungi dati</string> + <string name="profile_metadata_label_label">Etichetta</string> + <string name="profile_metadata_content_label">Contenuto</string> + <string name="pref_title_absolute_time">Usa ora assoluta</string> + <string name="label_remote_account">Il profilo dell\'utente mostrato qui sotto potrebbe essere incompleto. Premi per aprire il profilo completo nel browser.</string> + <string name="unpin_action">Non fissare in cima</string> + <string name="pin_action">Fissa</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Preferito</item> + <item quantity="many"><b>%1$s</b> Preferiti</item> + <item quantity="other"><b>%1$s</b> Preferiti</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> ricondivisione</item> + <item quantity="many"><b>%1$s</b> ricondivisioni</item> + <item quantity="other"><b>%1$s</b> ricondivisioni</item> + </plurals> + <string name="title_reblogged_by">Condiviso da</string> + <string name="title_favourited_by">Aggiunto ai preferiti da</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s e %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s ed altri %3$d</string> + <string name="description_post_media">Media: %1$s</string> + <string name="description_post_cw">Contenuto sensibile: %1$s</string> + <string name="description_post_media_no_description_placeholder">Nessuna descrizione</string> + <string name="description_post_reblogged">Ribloggato</string> + <string name="description_post_favourited">Messo nei preferiti</string> + <string name="description_visibility_public"> Pubblico + </string> + <string name="description_visibility_unlisted">Non in elenco</string> + <string name="description_visibility_private">Solo seguaci</string> + <string name="description_visibility_direct"> Diretti + </string> + <string name="hint_list_name">Nome della lista</string> + <string name="download_media">Scarica media</string> + <string name="downloading_media">Scaricando media</string> + <string name="compose_shortcut_long_label">Componi post</string> + <string name="edit_hashtag_hint">Hashtag senza #</string> + <string name="compose_shortcut_short_label">Componi</string> + <string name="notifications_clear">Cancella</string> + <string name="notifications_apply_filter">Filtra</string> + <string name="filter_apply">Applica</string> + <string name="pref_title_bot_overlay">Mostra indicatore bot</string> + <string name="notification_clear_text">Sei sicuro di voler permanentemente eliminare tutte le tue notifiche\?</string> + <string name="action_delete_and_redraft">Cancella e riscrivi</string> + <string name="dialog_redraft_post_warning">Cancellare e riscrivere questo post\?</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s voto</item> + <item quantity="many">%1$s voti</item> + <item quantity="other">%1$s voti</item> + </plurals> + <string name="poll_info_time_absolute">si conclude alle %1$s</string> + <string name="poll_info_closed">concluso</string> + <string name="poll_vote">Vota</string> + <string name="title_domain_mutes">Domini nascosti</string> + <string name="action_view_domain_mutes">Domini nascosti</string> + <string name="action_mute_domain">Silenzia %1$s</string> + <string name="confirmation_domain_unmuted">%1$s mostrati</string> + <string name="mute_domain_warning">Sei sicuro di voler bloccare tutto %1$s\? Non vedrai nessun contenuto da quel dominio in nessuna timeline pubblica o nelle tue notifiche. I tuoi seguaci che stanno in quel dominio saranno rimossi.</string> + <string name="mute_domain_warning_dialog_ok">Nascondi l\'intero dominio</string> + <string name="pref_title_notification_filter_poll">dei sondaggi si sono conclusi</string> + <string name="pref_title_animate_gif_avatars">Riproduci avatar animati</string> + <string name="notification_poll_name">Votazioni</string> + <string name="notification_poll_description">Notifiche sui sondaggi che si sono conclusi</string> + <string name="filter_dialog_whole_word">Parola intera</string> + <string name="filter_dialog_whole_word_description">Quando la parola chiave o la frase sono composte da soli caratteri alfanumerici, sarà applicata solo se corrisponde alla parola completa</string> + <string name="caption_notoemoji">Set di emoji di Google</string> + <string name="title_bookmarks">Segnalibri</string> + <string name="action_bookmark">Segnalibro</string> + <string name="action_edit">Modifica</string> + <string name="action_view_bookmarks">Segnalibri</string> + <string name="action_add_poll">Aggiungi sondaggio</string> + <string name="about_powered_by_tusky">Fatto usando Tusky</string> + <string name="pref_title_alway_open_spoiler">Espandi sempre i post segnalati come contenuto sensibile</string> + <string name="description_post_bookmarked">Messo nei segnalibri</string> + <string name="description_poll">Sondaggio con scelte: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="select_list_title">Scegli lista</string> + <string name="list">Lista</string> + <string name="compose_preview_image_description">Azioni per l\'immagine %1$s</string> + <string name="poll_ended_voted">Un sondaggio in cui hai votato si è concluso</string> + <string name="poll_ended_created">Un sondaggio che hai creato si è concluso</string> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d giorno rimasto</item> + <item quantity="many">%1$d giorni rimasti</item> + <item quantity="other">%1$d giorni rimasti</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d ora rimasta</item> + <item quantity="many">%1$d ore rimaste</item> + <item quantity="other">%1$d ore rimaste</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d minuto rimasto</item> + <item quantity="many">%1$d minuti rimasti</item> + <item quantity="other">%1$d minuti rimasti</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d secondo rimasto</item> + <item quantity="many">%1$d secondi rimasti</item> + <item quantity="other">%1$d secondi rimasti</item> + </plurals> + <string name="button_continue">Continua</string> + <string name="button_back">Indietro</string> + <string name="button_done">Fatto</string> + <string name="report_sent_success">Segnalato @%1$s con successo</string> + <string name="hint_additional_info">Altri commenti</string> + <string name="report_remote_instance">Inoltra a %1$s</string> + <string name="failed_report">Segnalazione fallita</string> + <string name="failed_fetch_posts">Scaricamento dei post fallito</string> + <string name="report_description_1">La segnalazione sarà inviata al moderatore del tuo server. Puoi spiegare perchè stai segnalando l\'utente qui sotto:</string> + <string name="report_description_remote_instance">L\'utente è su un altro server. Mandare una copia della segnalazione anche lì\?</string> + <string name="title_accounts">Utenti</string> + <string name="failed_search">Errore durante la ricerca</string> + <string name="create_poll_title">Sondaggio</string> + <string name="duration_5_min">5 minuti</string> + <string name="duration_30_min">30 minuti</string> + <string name="duration_1_hour">1 ora</string> + <string name="duration_6_hours">6 ore</string> + <string name="duration_1_day">1 giorno</string> + <string name="duration_3_days">3 giorni</string> + <string name="duration_7_days">7 giorni</string> + <string name="add_poll_choice">Aggiungi scelta</string> + <string name="poll_allow_multiple_choices">Scelte multiple</string> + <string name="poll_new_choice_hint">Scelta %1$d</string> + <string name="edit_poll">Modifica</string> + <string name="post_lookup_error_format">Errore nella ricerca del post %1$s</string> + <string name="title_scheduled_posts">Post programmati</string> + <string name="action_access_scheduled_posts">Post programmati</string> + <string name="action_schedule_post">Programma un post</string> + <string name="action_reset_schedule">Ripristina</string> + <string name="poll_info_format"> \u0020<!-- 15 voti • 1 ora mancante --> \u0020%1$s • %2$s</string> + <string name="no_drafts">Non hai bozze.</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s persona</item> + <item quantity="many">%1$s persone</item> + <item quantity="other">%1$s persone</item> + </plurals> + <string name="hashtags">Hashtag</string> + <string name="add_hashtag_title">Aggiungi hashtag</string> + <string name="dialog_mute_warning">Silenziare @%1$s\?</string> + <string name="dialog_block_warning">Bloccare @%1$s\?</string> + <string name="action_unmute_domain">Smetti di silenziare %1$s</string> + <string name="action_unmute_conversation">Smetti di silenziare la conversazione</string> + <string name="action_mute_conversation">Silenzia conversazione</string> + <string name="notification_follow_request_format">%1$s ha chiesto di seguirti</string> + <string name="action_unmute_desc">Smetti di silenziare %1$s</string> + <string name="notification_follow_request_name">Richieste di seguirti</string> + <string name="account_note_saved">Salvato!</string> + <string name="account_note_hint">La tua nota privata su questo account</string> + <string name="pref_title_hide_top_toolbar">Nascondi il titolo della barra degli strumenti in alto</string> + <string name="pref_title_confirm_reblogs">Chiedi conferma prima di condividere</string> + <string name="pref_title_show_cards_in_timelines">Mostra le anteprime dei collegamenti nelle timeline</string> + <string name="warning_scheduling_interval">Mastodon ha un intervallo di programmazione minimo di 5 minuti.</string> + <string name="no_announcements">Non ci sono annunci.</string> + <string name="no_scheduled_posts">Non hai post programmati.</string> + <string name="pref_title_enable_swipe_for_tabs">Abilita il gesto di scorrimento per passare da una scheda all\'altra</string> + <string name="notification_follow_request_description">Notifiche sulle richieste di essere seguiti</string> + <string name="pref_main_nav_position_option_bottom">In fondo</string> + <string name="pref_main_nav_position_option_top">In cima</string> + <string name="pref_main_nav_position">Posizione barra di navigazione principale</string> + <string name="pref_title_gradient_for_media">Mostra gradienti colorati per i media nascosti</string> + <string name="dialog_mute_hide_notifications">Nascondi notifiche</string> + <string name="title_announcements">Annunci</string> + <string name="pref_title_notification_filter_follow_requests">mi viene richiesto di seguirmi</string> + <string name="wellbeing_hide_stats_profile">Nascondi statistiche quantitative sui profili</string> + <string name="wellbeing_hide_stats_posts">Nascondi le statistiche quantitative sui post</string> + <string name="limit_notifications">Limita le notifiche della timeline</string> + <string name="review_notifications">Rivedi le notifiche</string> + <string name="pref_title_wellbeing_mode">Benessere</string> + <string name="notification_subscription_description">Notifiche di nuovi post di qualcuno a cui sei iscritto</string> + <string name="notification_subscription_name">Nuovi post</string> + <string name="pref_title_notification_filter_subscriptions">qualcuno che seguo ha pubblicato un nuovo post</string> + <string name="notification_subscription_format">%1$s ha appena pubblicato</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Non puoi caricare più di %1$d allegato multimediale.</item> + <item quantity="many">Non puoi caricare più di %1$d allegati multimediali.</item> + <item quantity="other">Non puoi caricare più di %1$d allegati multimediali.</item> + </plurals> + <string name="drafts_post_reply_removed">Il post a cui hai scritto una bozza di risposta è stato rimosso</string> + <string name="draft_deleted">Bozza eliminata</string> + <string name="drafts_post_failed_to_send">L\'invio di questo post è fallito!</string> + <string name="dialog_delete_list_warning">Sei sicuro di voler cancellare la lista %1$s\?</string> + <string name="duration_indefinite">Indefinita</string> + <string name="label_duration">Durata</string> + <string name="post_media_attachments">Allegati</string> + <string name="post_media_audio">Audio</string> + <string name="pref_title_animate_custom_emojis">Riproduci emoji animate</string> + <string name="action_subscribe_account">Iscriviti</string> + <string name="dialog_delete_conversation_warning">Rimuovere questa conversazione\?</string> + <string name="drafts_failed_loading_reply">Errore nel recupero delle informazioni sulla risposta</string> + <string name="action_unsubscribe_account">Disiscriviti</string> + <string name="action_delete_conversation">Elimina conversazione</string> + <string name="wellbeing_mode_notice">Alcune informazioni che potrebbero influenzare il tuo benessere mentale saranno nascoste. Questo include: +\n +\n - Notifiche riguardo a Preferiti/Boost/Following +\n - Conteggio dei Preferiti/Boost nei post +\n - Statistiche riguardo a Preferiti/Post nei profili +\n +\n Le notifiche push non saranno influenzate, ma puoi modificare le tue impostazioni delle notifiche manualmente.</string> + <string name="action_unbookmark">Rimuovi segnalibro</string> + <string name="pref_title_confirm_favourites">Chiedi conferma prima di mettere nei preferiti</string> + <string name="duration_14_days">14 giorni</string> + <string name="duration_30_days">30 giorni</string> + <string name="duration_60_days">60 giorni</string> + <string name="duration_90_days">90 giorni</string> + <string name="duration_180_days">180 giorni</string> + <string name="duration_365_days">365 giorni</string> + <string name="follow_requests_info">Anche se il tuo account non è bloccato, lo staff di %1$s ha pensato che potresti voler verificare le richieste di seguirti da parte questi utenti manualmente.</string> + <string name="notification_sign_up_format">%1$s si è registrato</string> + <string name="pref_title_notification_filter_sign_ups">qualcuno si è registrato</string> + <string name="title_login">Accesso</string> + <string name="notification_update_format">%1$s ha modificato il suo post</string> + <string name="pref_title_notification_filter_updates">un post con cui ho interagito è stato modificato</string> + <string name="tusky_compose_post_quicksetting_label">Componi post</string> + <string name="notification_sign_up_name">Registrazioni</string> + <string name="notification_sign_up_description">Notifiche di quando qualcuno si è registrato</string> + <string name="notification_update_name">Modifiche ai post</string> + <string name="notification_update_description">Notifiche di quando i post con cui hai interagito vengono modificati</string> + <string name="error_could_not_load_login_page">Non è stato possibile caricare la pagina di accesso.</string> + <string name="action_edit_image">Modifica immagine</string> + <string name="saving_draft">Salvataggio bozza…</string> + <string name="action_dismiss">Scartare</string> + <string name="action_details">Dettagli</string> + <string name="tips_push_notification_migration">Riaccedi a tutti le utenze per attivare il supporto delle notifiche.</string> + <string name="dialog_push_notification_migration">Al fine di utilizzare le notifiche tramite UnifiedPush, Tusky ha bisogno del permesso di sottoscrivere alle notifiche nella tua istanza Mastodon. Questo richiede un nuovo accesso per cambiare l\'OAuth precedentemente concesso a Tusky. Usare questa opzione qui o nelle preferenze dell\'account preserva tutte le tue bozze locali e la memoria temporanea (cache).</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="dialog_push_notification_migration_other_accounts">Nuovo accesso eseguito per l\'utenza corrente al fine di garantire il permesso delle notifiche a Tusky. Però hai altre utenze che non sono state migrate in questo modo. Cambia utenza e riaccedi una alla volta per abilitare il supporto alle notifiche UnifiedPush.</string> + <string name="account_date_joined">Registrato da %1$s</string> + <string name="error_multimedia_size_limit">Video and audio files non possono eccedere %1$s MB in dimensione.</string> + <string name="error_image_edit_failed">L\'immagine non può essere modificata.</string> + <string name="title_migration_relogin">Riaccedi per le notifiche</string> + <string name="error_following_hashtag_format">Errore provando a seguire #%1$s</string> + <string name="error_unfollowing_hashtag_format">Errore smettendo di provare a seguire #%1$s</string> + <string name="status_count_one_plus">1+</string> + <string name="error_loading_account_details">Caricamento dettagli utente fallito</string> + <string name="delete_scheduled_post_warning">Cancellare questo post programmato\?</string> + <string name="instance_rule_title">Regole di %1$s</string> + <string name="instance_rule_info">Facendo il log in accetti le regole di %1$s.</string> + <string name="set_focus_description">Tappa o crea un cerchio per scegliere il punto focale che sarà sempre visibile nelle anteprime.</string> + <string name="action_set_focus">Imposta punto focale</string> + <string name="description_post_language">Lingua del post</string> + <string name="pref_show_self_username_always">Sempre</string> + <string name="pref_show_self_username_never">Mai</string> + <string name="pref_show_self_username_disambiguate">Quando connesso con più account</string> + <string name="duration_no_change">(nessuna modifica)</string> + <string name="pref_title_show_self_username">Mostra nome utente nelle barre strumenti</string> + <string name="action_add_or_remove_from_list">Aggiunti o rimuovi dalla lista</string> + <string name="failed_to_add_to_list">Aggiunta dell\'account alla lista fallita</string> + <string name="failed_to_remove_from_list">Rimozione dell\'account dalla lista fallita</string> + <string name="compose_save_draft_loses_media">Salvare bozza\? (gli allegati verranno ricaricati quando ripristini la bozza.)</string> + <string name="failed_to_pin">Fissaggio fallito</string> + <string name="hint_media_description_missing">I media dovrebbero avere una descrizione.</string> + <string name="no_lists">Non hai alcuna lista.</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="pref_default_post_language">Lingua predefinita per i post creati (sincronizzato sul server)</string> + <string name="notification_report_description">Notifiche sulle segnalazioni da moderare</string> + <string name="failed_to_unpin">Rimozione del fissaggio fallita</string> + <string name="pref_title_http_proxy_port_message">Il port dovrebbe essere tra %1$d e %2$d</string> + <string name="report_category_violation">Violazione di una regola</string> + <string name="status_created_at_now">adesso</string> + <string name="post_media_alt">ALT</string> + <string name="pref_title_notification_filter_reports">c\'è una nuova segnalazione</string> + <string name="notification_report_name">Segnalazioni</string> + <string name="error_muting_hashtag_format">Errore nel silenziamento di #%1$s</string> + <string name="error_unmuting_hashtag_format">Errore nella rimozione del silenziamento di #%1$s</string> + <string name="title_followed_hashtags">Hashtag seguiti</string> + <string name="description_post_edited">Modificato</string> + <string name="error_following_hashtags_unsupported">Questa istanza non permette di seguire gli hashtag.</string> + <string name="action_discard">Scarta modifiche</string> + <string name="action_continue_edit">Continua a modificare</string> + <string name="notification_report_format">Nuova segnalazione su %1$s</string> + <string name="notification_header_report_format">%1$s ha segnalato %2$s</string> + <string name="notification_summary_report_format">%1$s · %2$d post allegati</string> + <string name="action_add_reaction">aggiungi reazione</string> + <string name="compose_unsaved_changes">Hai delle modifiche non salvate.</string> + <string name="confirmation_hashtag_unfollowed">Non segui più #%1$s</string> + <string name="error_status_source_load">Caricamento dello status della sorgente dal server fallito.</string> + <string name="post_edited">Modificato %1$s</string> + <string name="mute_notifications_switch">Silenzia notifiche</string> + <string name="title_edits">Modifiche</string> + <string name="report_category_spam">Spam</string> + <string name="status_edit_info">%1$s ha modificato</string> + <string name="status_created_info">%1$s ha creato</string> + <string name="report_category_other">Altro</string> + <string name="action_unfollow_hashtag_format">Smettere di seguire #%1$s\?</string> + <string name="action_post_failed">Caricamento fallito</string> + <string name="action_browser_login">Accedi dal browser</string> + <string name="a11y_label_loading_thread">Carico il thread</string> + <string name="pref_summary_http_proxy_disabled">Disattivato</string> + <string name="pref_summary_http_proxy_missing"><non impostato></string> + <string name="pref_summary_http_proxy_invalid"><non valido></string> + <string name="pref_reading_order_newest_first">Più nuovi prima</string> + <string name="action_post_failed_detail">Non è stato possibile caricare il tuo post ed è stato salvato nelle bozze. +\n +\nNon è stato possibile contattare il server oppure il server ha rifiutato il post.</string> + <string name="action_post_failed_detail_plural">Non è stato possibile caricare i tuoi post e sono stati salvati nelle bozze. +\n +\nNon è stato possibile contattare il server oppure il server ha rifiutato i post.</string> + <string name="action_post_failed_show_drafts">Mostra bozze</string> + <string name="action_post_failed_do_nothing">Chiudi</string> + <string name="send_account_username_to">Condividi nome utente dell\'account a…</string> + <string name="pref_title_reading_order">Ordine di lettura</string> + <string name="description_login">Funziona nella maggior parte dei casi. Nessun dato è trasferito ad altre app.</string> + <string name="description_browser_login">Potrebbe supportare differenti metodi di autenticazione, ma richiede un browser supportato.</string> + <string name="pref_reading_order_oldest_first">Più vecchi prima</string> + <string name="action_share_account_link">Condividi link dell\'account</string> + <string name="action_share_account_username">Condividi il nome utente dell\'account</string> + <string name="send_account_link_to">Condividi URL account a…</string> + <string name="account_username_copied">Nome utente copiato</string> + <string name="dialog_follow_hashtag_title">Segui hashtag</string> + <string name="dialog_follow_hashtag_hint">#hashtag</string> + <string name="accessibility_talking_about_tag">%1$d persone parlano dell\'hashtag %2$s</string> + <string name="total_usage">Utilizzo totale</string> + <string name="total_accounts">Account totali</string> + <string name="action_refresh">Ricarica</string> + <string name="title_public_trending_hashtags">Hashtag di tendenza</string> + <string name="help_empty_home">Questa è la tua <b>Timeline principale</b>. Mostra i post più recenti degli account che segui. +\n +\nPer esplorare gli account puoi scopririli in una delle altre timeline. Per esempio la timeline locale della tua istanza [iconica gmd_group]. O puoi cercarli per fine [iconica gmd_search]; per esempio cerca “Tusky” per trovare il nostro account Mastodon.</string> + <string name="notification_unknown_name">Sconosciuto</string> + <string name="post_media_image">Immagine</string> + <string name="label_filter_title">Titolo</string> + <string name="filter_action_warn">Avvisa</string> + <string name="filter_action_hide">Nascondi</string> + <string name="ui_error_bookmark">Salvataggio nei segnalibri fallito: %1$s</string> + <string name="socket_timeout_exception">Contattare il tuo server ha richiesto troppo tempo</string> + <string name="ui_error_unknown">motivo sconosciuto</string> + <string name="ui_error_clear_notifications">Cancellazione notifiche fallita: %1$s</string> + <string name="ui_error_favourite">Impossibile mettere nei preferiti: %1$s</string> + <string name="ui_error_reblog">Condivisione del post fallita: %1$s</string> + <string name="ui_error_reject_follow_request">Rifiuto richiesta di follow fallita: %1$s</string> + <string name="ui_error_vote">Votazione al sondaggio fallita: %1$s</string> + <string name="ui_error_accept_follow_request">Accettazione richiesta di follow fallita: %1$s</string> + <string name="ui_success_accepted_follow_request">Richiesta di follow accettata</string> + <string name="ui_success_rejected_follow_request">Richiesta di follow bloccata</string> + <string name="select_list_manage">Gestisci liste</string> + <string name="status_filtered_show_anyway">Mostra comunque</string> + <string name="status_filter_placeholder_label_format">Filtrato: %1$s</string> + <string name="pref_title_account_filter_keywords">Profili</string> + <string name="hint_filter_title">I miei filtri</string> + <string name="action_add">Aggiungi</string> + <string name="pref_title_show_stat_inline">Mostra le statistiche del post nella timeline</string> + <string name="filter_description_warn">Nascondi con un avviso</string> + <string name="filter_keyword_display_format">%1$s (parola intera)</string> + <string name="filter_description_hide">Nascondi completamente</string> + <string name="label_filter_action">Azioni del filtro</string> + <string name="label_filter_context">Contesti di filtraggio</string> + <string name="label_filter_keywords">Parole chiave i frasi da filtrare</string> + <string name="filter_keyword_addition_title">Aggiungi parola chiave</string> + <string name="filter_edit_keyword_title">Modifica parola chiave</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="load_newest_notifications">Carica le notifiche più recenti</string> + <string name="compose_delete_draft">Cancellare bozza\?</string> + <string name="error_missing_edits">Il tuo server sa che questo post è stato modificato, ma non ha una copia delle modifiche, quindi non ti può essere mostrato. +\n +\nQuesto è il <a href="https://github.com/mastodon/mastodon/issues/25398">problema Mastodon #25398</a>.</string> + <string name="pref_ui_text_size">Dimensione testo dell\'UI</string> + <string name="notification_listenable_worker_name">Attività in secondo piano</string> + <string name="notification_listenable_worker_description">Notifiche mentre Tusky è attivo in secondo piano</string> + <string name="notification_notification_worker">Recupero notifiche…</string> + <string name="notification_prune_cache">Manutenzione cache…</string> + <string name="about_device_info_title">Il tuo dispositivo</string> + <string name="about_device_info">%1$s %2$s +\nVersione Android: %3$s +\nVersione SDK: %4$d</string> + <string name="about_account_info_title">Il tuo account</string> + <string name="about_account_info">\@%1$s@%2$s +\nVersione: %3$s</string> + <string name="list_exclusive_label">Nascondi dalla timeline principale</string> + <string name="error_media_playback">Playback fallito: %1$s</string> + <string name="dialog_delete_filter_text">Cancellare filtro \'%1$s\'\?</string> + <string name="dialog_delete_filter_positive_action">Cancella</string> + <string name="about_copy">Copia versione e informazioni dispositivo</string> + <string name="about_copied">Versione e informazioni dispositivo copiate</string> + <string name="dialog_save_profile_changes_message">Vuoi salvare le modifiche al tuo profilo\?</string> + <string name="error_media_upload_sending_fmt">Il caricamento è fallito: %1$s</string> + <string name="label_image">Immagine</string> + <string name="app_theme_system_black">Usa design di sistema (nero)</string> + <string name="help_empty_conversations">Questi sono i tuoi <b>messaggi privati</b>; a volte sono chiamati conversazioni o messaggi diretti (DM). +\n +\nI messaggi privati sono creati impostando la visibilità [iconics gmd_public] di un post su [iconics gmd_mail] <i>Privato</i> e menzionando uno o più utenti nel testo. +\n +\nPer esempio puoi andare su un profilo e creare un messaggio [iconics gmd_edit] e cambiare la visibilità. \u0020</string> + <string name="unfollowing_hashtag_success_format">Non segui più l\'hashtag #%1$s</string> + <string name="unmuting_hashtag_success_format">Riattivazione dell\'hashtag #%1$s</string> + <string name="action_view_filter">Vedi filtro</string> + <string name="following_hashtag_success_format">Ora segui l\'hashtag #%1$s</string> + <string name="muting_hashtag_success_format">Hashtag #%1$s silenziato come avvertimento</string> + <string name="help_empty_lists">Questa è la <b>vista delle tue liste</b>. Puoi definire un numero di liste private a cui aggiungere account. +\n +\nRICORDA che puoi aggiungere alle liste solo account che già segui. +\n +\nQueste liste possono essere utilizzate come scheda dalle Impostazioni account [iconics gmd_account_circle] [iconics gmd_navigate_next] Schede \u0020</string> + <string name="title_public_trending_statuses">Post di tendenza</string> + <string name="error_blocking_domain">Silenziamento di %1$s fallito: %2$s</string> + <string name="error_unblocking_domain">Riattivazione di %1$s fallito: %2$s</string> + <string name="pref_title_show_self_boosts">Mostra boost ai tuoi post</string> + <string name="list_reply_policy_none">Nessuno</string> + <string name="list_reply_policy_list">Membri della lista</string> + <string name="list_reply_policy_followed">Qualsiasi utente seguito</string> + <string name="list_reply_policy_label">Mostra risposte a</string> + <string name="pref_title_show_self_boosts_description">Qualcuno boosta il proprio post</string> + <string name="reply_sending">Invio…</string> + <string name="reply_sending_long">La tua risposta viene inviata.</string> + <string name="action_translate">Traduci</string> + <string name="action_show_original">Mostra originale</string> + <string name="label_translating">Traduzione…</string> + <string name="label_translated">Tradotto da %1$s con %2$s</string> + <string name="ui_error_translate">Impossibile tradurre: %1$s</string> + <string name="pref_title_per_timeline_preferences">Preferenze per timeline</string> + <string name="pref_title_show_notifications_filter">Mostra filtro notifiche</string> + <string name="dialog_follow_warning">Seguire questo account?</string> + <string name="report_category_legal">Legale</string> + <string name="pref_title_confirm_follows">Mostra conferma prima di seguire</string> + <string name="unknown_notification_type">Tipo di notifica sconosciuto</string> + <string name="url_copied">URL copiato</string> + <string name="confirmation_hashtag_copied">\'#%1$s\' copiato</string> + <string name="pref_default_reply_privacy">Privacy predefinita della risposta (non sincronizzato sul server)</string> + <string name="error_deleting_filter">Errore nella cancellazione del filtro \'%1$s\'</string> + <string name="error_saving_filter">Errore nel salvataggio del filtro \'%1$s\'</string> + <string name="action_follow_hashtag">Segui un nuovo hashtag</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/main/res/values-iw/strings.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources></resources> \ No newline at end of file diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml new file mode 100644 index 0000000..01d4d49 --- /dev/null +++ b/app/src/main/res/values-ja/strings.xml @@ -0,0 +1,675 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">エラーが発生しました。</string> + <string name="error_empty">本文なしでは投稿できません。</string> + <string name="error_invalid_domain">無効なドメインです</string> + <string name="error_failed_app_registration">そのインスタンスでの認証に失敗しました。失敗が続く場合、メニューからブラウザでのログインを試してください。</string> + <string name="error_no_web_browser_found">ウェブブラウザが見つかりませんでした。</string> + <string name="error_authorization_unknown">不明な承認エラーが発生しました。失敗が続く場合、メニューからブラウザでのログインを試してください。</string> + <string name="error_authorization_denied">認証が拒否されました。正しい認証情報を入力したことが確かな場合、メニューからブラウザでのログインを試してください。</string> + <string name="error_retrieving_oauth_token">ログイントークンの取得に失敗しました。もし失敗が続く場合、メニューからブラウザでのログインを試してください。</string> + <string name="error_compose_character_limit">投稿文が長すぎます!</string> + <string name="error_media_upload_type">その形式のファイルはアップロードできません。</string> + <string name="error_media_upload_opening">ファイルを開けませんでした。</string> + <string name="error_media_upload_permission">メディアの読み取り許可が必要です。</string> + <string name="error_media_download_permission">メディアの書き込み許可が必要です。</string> + <string name="error_media_upload_image_or_video">画像と動画を同時に投稿することはできません。</string> + <string name="error_media_upload_sending">アップロードに失敗しました。</string> + <string name="error_sender_account_gone">投稿送信時のエラーです。</string> + <string name="title_home">ホーム</string> + <string name="title_notifications">通知</string> + <string name="title_public_local">ローカル</string> + <string name="title_public_federated">連合</string> + <string name="title_direct_messages">ダイレクトメッセージ</string> + <string name="title_tab_preferences">タブ</string> + <string name="title_view_thread">スレッド</string> + <string name="title_posts">投稿</string> + <string name="title_posts_with_replies">投稿と返信</string> + <string name="title_posts_pinned">ピン留め</string> + <string name="title_follows">フォロー</string> + <string name="title_followers">フォロワー</string> + <string name="title_favourites">お気に入り</string> + <string name="title_mutes">ミュートしたユーザー</string> + <string name="title_blocks">ブロックしたユーザー</string> + <string name="title_follow_requests">フォローリクエスト</string> + <string name="title_edit_profile">プロフィールを編集</string> + <string name="title_drafts">下書き</string> + <string name="title_licenses">ライセンス</string> + <string name="post_boosted_format">%1$sさんがブーストしました</string> + <string name="post_sensitive_media_title">閲覧注意</string> + <string name="post_media_hidden_title">非表示のメディア</string> + <string name="post_sensitive_media_directions">タップして表示</string> + <string name="post_content_warning_show_more">続きを表示</string> + <string name="post_content_warning_show_less">続きを隠す</string> + <string name="post_content_show_more">続きを読む</string> + <string name="post_content_show_less">閉じる</string> + <string name="message_empty">何もありません。</string> + <string name="footer_empty">現在投稿はありません。更新するにはプルダウンしてください!</string> + <string name="notification_reblog_format">%1$sさんが投稿をブーストしました</string> + <string name="notification_favourite_format">%1$sさんが投稿をお気に入りに登録しました</string> + <string name="notification_follow_format">%1$sさんにフォローされました</string> + <string name="report_username_format">\@%1$sさんを通報</string> + <string name="report_comment_hint">コメント</string> + <string name="action_quick_reply">クイック返信</string> + <string name="action_reply">返信</string> + <string name="action_reblog">ブースト</string> + <string name="action_favourite">お気に入り</string> + <string name="action_more">その他</string> + <string name="action_compose">投稿する</string> + <string name="action_login">Tusky でログイン</string> + <string name="action_logout">ログアウト</string> + <string name="action_logout_confirm">アカウント %1$s からログアウトしてもよろしいですか?ログアウトすると、下書きや詳細設定を含めて、ローカルに保存されているアカウントの全てのデータが削除されます。</string> + <string name="action_follow">フォローする</string> + <string name="action_unfollow">フォロー解除</string> + <string name="action_block">ブロック</string> + <string name="action_unblock">ブロック解除</string> + <string name="action_hide_reblogs">ブーストを非表示</string> + <string name="action_show_reblogs">ブーストを表示</string> + <string name="action_report">通報</string> + <string name="action_delete">削除</string> + <string name="action_delete_and_redraft">削除して編集</string> + <string name="action_send">トゥート</string> + <string name="action_send_public">トゥート!</string> + <string name="action_retry">再試行</string> + <string name="action_close">閉じる</string> + <string name="action_view_profile">プロフィール</string> + <string name="action_view_preferences">設定</string> + <string name="action_view_account_preferences">アカウント設定</string> + <string name="action_view_favourites">お気に入り</string> + <string name="action_view_mutes">ミュートしたユーザー</string> + <string name="action_view_blocks">ブロックしたユーザー</string> + <string name="action_view_follow_requests">フォローリクエスト</string> + <string name="action_view_media">メディア</string> + <string name="action_open_in_web">ブラウザで開く</string> + <string name="action_add_media">メディアを追加</string> + <string name="action_photo_take">写真を撮る</string> + <string name="action_share">共有</string> + <string name="action_mute">ミュート</string> + <string name="action_unmute">ミュート解除</string> + <string name="action_mention">返信</string> + <string name="action_hide_media">メディアを隠す</string> + <string name="action_open_drawer">メニューを開く</string> + <string name="action_save">保存</string> + <string name="action_edit_profile">プロフィールを編集</string> + <string name="action_edit_own_profile">編集</string> + <string name="action_undo">取り消す</string> + <string name="action_accept">許可</string> + <string name="action_reject">拒否</string> + <string name="action_search">検索</string> + <string name="action_access_drafts">下書き</string> + <string name="action_toggle_visibility">投稿の公開範囲</string> + <string name="action_content_warning">注意書き</string> + <string name="action_emoji_keyboard">絵文字キーボード</string> + <string name="action_add_tab">タブの追加</string> + <string name="action_links">リンク</string> + <string name="action_hashtags">ハッシュタグ</string> + <string name="title_hashtags_dialog">ハッシュタグ</string> + <string name="title_links_dialog">リンク</string> + <string name="download_image">%1$s をダウンロードしています</string> + <string name="action_copy_link">リンクをコピー</string> + <string name="action_open_as">%1$s として開く</string> + <string name="action_share_as">共有先…</string> + <string name="send_post_link_to">投稿URLを共有…</string> + <string name="send_post_content_to">投稿を共有…</string> + <string name="send_media_to">メディアを共有…</string> + <string name="confirmation_reported">送信しました!</string> + <string name="confirmation_unblocked">ブロックを解除しました</string> + <string name="confirmation_unmuted">ミュートを解除しました</string> + <string name="hint_domain">インスタンス</string> + <string name="hint_compose">今なにしてる?</string> + <string name="hint_content_warning">注意書き</string> + <string name="hint_display_name">表示名</string> + <string name="hint_note">プロフィール</string> + <string name="hint_search">ユーザーを検索…</string> + <string name="search_no_results">見つかりませんでした</string> + <string name="label_quick_reply">返信…</string> + <string name="label_avatar">アイコン</string> + <string name="label_header">ヘッダー</string> + <string name="link_whats_an_instance">インスタンスとは?</string> + <string name="login_connection">接続中…</string> + <string name="dialog_whats_an_instance">mastodon.social, icosahedron.website, social.tchncs.deや<a href="https://instances.social">その他</a> のような、あらゆるインスタンスのアドレスやドメインを入力できます。 +\n +\nまだアカウントをお持ちでない場合は、参加したいインスタンスの名前を入力することで そのインスタンスにアカウントを作成できます。 +\n +\nインスタンスはあなたのアカウントが提供される単独の場所ですが、他のインスタンスのユーザーとあたかも同じ場所にいるように簡単にコミュニケーションをとったりフォローしたりできます。 +\n +\nさらに詳しい情報は<a href="https://joinmastodon.org">joinmastodon.org</a>でご覧いただけます。 </string> + <string name="dialog_title_finishing_media_upload">メディアをアップロードしています</string> + <string name="dialog_message_uploading_media">アップロード中…</string> + <string name="dialog_download_image">ダウンロード</string> + <string name="dialog_message_cancel_follow_request">フォローリクエストを取り消しますか?</string> + <string name="dialog_unfollow_warning">このアカウントをフォロー解除しますか?</string> + <string name="dialog_delete_post_warning">本当に削除しますか?</string> + <string name="visibility_public">公開:公開タイムラインに投稿する</string> + <string name="visibility_unlisted">未収載:公開タイムラインには表示しない</string> + <string name="visibility_private">フォロワー限定:フォロワーだけに公開</string> + <string name="visibility_direct">ダイレクト:返信先のユーザーだけに公開</string> + <string name="pref_title_edit_notification_settings">通知</string> + <string name="pref_title_notifications_enabled">通知</string> + <string name="pref_title_notification_alerts">通知時の動作</string> + <string name="pref_title_notification_alert_sound">着信音を鳴らす</string> + <string name="pref_title_notification_alert_vibrate">バイブレーションする</string> + <string name="pref_title_notification_alert_light">通知ランプ</string> + <string name="pref_title_notification_filters">通知の種類</string> + <string name="pref_title_notification_filter_mentions">返信</string> + <string name="pref_title_notification_filter_follows">フォロー</string> + <string name="pref_title_notification_filter_reblogs">投稿がブーストされた</string> + <string name="pref_title_notification_filter_favourites">投稿がお気に入りに登録された</string> + <string name="pref_title_appearance_settings">表示</string> + <string name="pref_title_app_theme">アプリテーマ</string> + <string name="pref_title_timelines">タイムライン</string> + <string name="pref_title_timeline_filters">フィルター</string> + <string name="app_them_dark">ダーク</string> + <string name="app_theme_light">ライト</string> + <string name="app_theme_black">ブラック</string> + <string name="app_theme_auto">日没による自動設定</string> + <string name="pref_title_browser_settings">ブラウザ</string> + <string name="pref_title_custom_tabs">Chrome Custom Tabsを使用する</string> + <string name="pref_title_language">言語</string> + <string name="pref_title_post_filter">タイムラインのフィルタリング</string> + <string name="pref_title_post_tabs">タブ</string> + <string name="pref_title_show_boosts">ブーストを表示</string> + <string name="pref_title_show_replies">返信を表示</string> + <string name="pref_title_show_media_preview">メディアのプレビューを表示する</string> + <string name="pref_title_proxy_settings">プロキシー</string> + <string name="pref_title_http_proxy_settings">HTTPプロキシー</string> + <string name="pref_title_http_proxy_enable">HTTPプロキシーを有効化</string> + <string name="pref_title_http_proxy_server">HTTPプロキシーサーバー</string> + <string name="pref_title_http_proxy_port">HTTPプロキシーポート</string> + <string name="pref_default_post_privacy">デフォルトの投稿プライバシー</string> + <string name="pref_default_media_sensitivity">メディアを常に閲覧注意としてマークする</string> + <string name="pref_publishing">投稿</string> + <string name="pref_failed_to_sync">設定の同期に失敗しました</string> + <string name="post_privacy_public">公開</string> + <string name="post_privacy_unlisted">未収載</string> + <string name="post_privacy_followers_only">フォロワーに限定</string> + <string name="pref_post_text_size">投稿のテキストサイズ</string> + <string name="post_text_size_smallest">最小</string> + <string name="post_text_size_small">小</string> + <string name="post_text_size_medium">中</string> + <string name="post_text_size_large">大</string> + <string name="post_text_size_largest">最大</string> + <string name="notification_mention_name">新しい返信</string> + <string name="notification_mention_descriptions">新しい返信の通知</string> + <string name="notification_follow_name">新しいフォロワー</string> + <string name="notification_follow_description">新しいフォロワーの通知</string> + <string name="notification_boost_name">ブースト</string> + <string name="notification_boost_description">あなたの投稿がブーストされたときの通知</string> + <string name="notification_favourite_name">お気に入り</string> + <string name="notification_favourite_description">あなたの投稿がお気に入りに登録されたときの通知</string> + <string name="notification_mention_format">%1$sさんが返信しました</string> + <string name="notification_summary_large">%1$sさん、%2$sさん、%3$sさんと他%4$d人</string> + <string name="notification_summary_medium">%1$sさん、%2$sさん、%3$sさん</string> + <string name="notification_summary_small">%1$sさん、%2$sさん</string> + <plurals name="notification_title_summary"> + <item quantity="other">%1$d件の新しい通知</item> + </plurals> + <string name="description_account_locked">非公開アカウント</string> + <string name="about_title_activity">このアプリについて</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tuskyは無料のオープンソースソフトウェアです。<!-- + -->GNU General Public License Version 3 の下で使用許諾されています。<!-- + -->ライセンスはここからご覧いただけます: https://www.gnu.org/licenses/gpl-3.0.ja.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">プロジェクトのウェブサイト (英語): https://tusky.app</string> + <string name="about_bug_feature_request_site">バグ報告と機能リクエスト (英語): https://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Tusky公式アカウント</string> + <string name="post_share_content">投稿の内容を共有</string> + <string name="post_share_link">投稿へのリンクを共有</string> + <string name="post_media_images">画像</string> + <string name="post_media_video">動画</string> + <string name="state_follow_requested">フォローリクエスト中</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="status_created_at_now">現在</string> + <string name="abbreviated_in_years">%1$d年後</string> + <string name="abbreviated_in_days">%1$d日後</string> + <string name="abbreviated_in_hours">%1$d時間後</string> + <string name="abbreviated_in_minutes">%1$d分後</string> + <string name="abbreviated_in_seconds">%1$d秒後</string> + <string name="abbreviated_years_ago">%1$d年前</string> + <string name="abbreviated_days_ago">%1$d日前</string> + <string name="abbreviated_hours_ago">%1$d時間前</string> + <string name="abbreviated_minutes_ago">%1$d分前</string> + <string name="abbreviated_seconds_ago">%1$d秒前</string> + <string name="follows_you">あなたをフォロー中</string> + <string name="pref_title_alway_show_sensitive_media">閲覧注意のメディアを常に表示</string> + <string name="title_media">メディア</string> + <string name="replying_to">\@%1$sに返信</string> + <string name="load_more_placeholder_text">さらに読み込む</string> + <string name="filter_addition_title">フィルターを追加</string> + <string name="filter_edit_title">フィルターを編集</string> + <string name="add_account_name">アカウントを追加</string> + <string name="add_account_description">新しいMastodonアカウントを追加</string> + <string name="action_lists">リスト</string> + <string name="title_lists">リスト</string> + <string name="error_rename_list">リスト名を更新できませんでした</string> + <string name="action_rename_list">リスト名の更新</string> + <string name="compose_active_account_description">%1$sとして投稿</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">視覚障害者のためのコンテキストを記入してください (%1$d文字まで)</item> + </plurals> + <string name="action_set_caption">説明を設定</string> + <string name="action_remove">消去</string> + <string name="lock_account_label">アカウントをロック</string> + <string name="lock_account_label_description">フォロワーを手動で承認する必要があります</string> + <string name="compose_save_draft">下書きを保存しますか?</string> + <string name="send_post_notification_title">投稿を送信中です…</string> + <string name="send_post_notification_error_title">投稿の送信エラー</string> + <string name="send_post_notification_channel_name">投稿の送信中</string> + <string name="send_post_notification_cancel_title">送信がキャンセルされました</string> + <string name="send_post_notification_saved_content">投稿のコピーが下書きに保存されました</string> + <string name="action_compose_shortcut">投稿する</string> + <string name="error_no_custom_emojis">インスタンス %1$s にはカスタム絵文字がありません</string> + <string name="emoji_style">絵文字スタイル</string> + <string name="system_default">システムのデフォルト</string> + <string name="download_fonts">最初にこれらの絵文字セットをダウンロードする必要があります</string> + <string name="performing_lookup_title">検索中…</string> + <string name="expand_collapse_all_posts">全て開く/閉じる</string> + <string name="action_open_post">投稿を開く</string> + <string name="restart_required">アプリの再起動が必要です</string> + <string name="restart_emoji">これらの変更を適用するには、Tuskyの再起動が必要になります</string> + <string name="later">後で</string> + <string name="restart">再起動</string> + <string name="caption_systememoji">あなたのデバイスのデフォルト絵文字セットです</string> + <string name="caption_blobmoji">Android 4.4-7.1で知られる、Blob絵文字です</string> + <string name="caption_twemoji">Mastodonの標準絵文字セットです</string> + <string name="download_failed">ダウンロード失敗</string> + <string name="profile_badge_bot_text">ボット</string> + <string name="account_moved_description">%1$s は引っ越しました:</string> + <string name="reblog_private">ブースト</string> + <string name="unreblog_private">ブーストを解除</string> + <string name="license_description">Tuskyは、以下のオープンソース プロジェクトからのコードとアセットを含んでいます:</string> + <string name="license_apache_2">Apache Licenseの下にライセンスされています(下記をコピー)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">プロフィール メタデータ</string> + <string name="profile_metadata_add">データを追加</string> + <string name="profile_metadata_label_label">ラベル</string> + <string name="profile_metadata_content_label">内容</string> + <string name="pref_title_absolute_time">絶対時間で表示</string> + <string name="label_remote_account">以下の情報は不正確な可能性があります。タップしてブラウザでプロフィールを開く</string> + <string name="unpin_action">固定表示を解除</string> + <string name="pin_action">プロフィールに固定</string> + <plurals name="favs"> + <item quantity="other"><b>%1$s</b> お気に入り</item> + </plurals> + <plurals name="reblogs"> + <item quantity="other"><b>%1$s</b> ブースト</item> + </plurals> + <string name="title_reblogged_by">ブーストした人物</string> + <string name="title_favourited_by">お気に入りした人物</string> + <string name="description_visibility_public"> 公開 + </string> + <string name="description_visibility_unlisted"> 未収載 + </string> + <string name="description_visibility_direct"> ダイレクト + </string> + <string name="hint_list_name">リスト名</string> + <string name="error_network">ネットワークエラーが発生しました。接続を確認してもう一度試してください。</string> + <string name="post_username_format">\@%1$s</string> + <string name="action_unreblog">ブーストを解除</string> + <string name="action_unfavourite">お気に入りを解除</string> + <string name="action_open_reblogged_by">ブーストを表示</string> + <string name="download_media">メディアをダウンロード</string> + <string name="downloading_media">メディアをダウンロード中</string> + <string name="app_theme_system">システムの設定を利用</string> + <string name="notifications_clear">削除</string> + <string name="notifications_apply_filter">フィルター</string> + <string name="notification_clear_text">すべての通知を完全に削除してよろしいですか?</string> + <plurals name="poll_info_votes"> + <item quantity="other">%1$s票</item> + </plurals> + <string name="poll_vote">投票</string> + <plurals name="poll_timespan_days"> + <item quantity="other">残り%1$d日</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="other">残り%1$d時間</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="other">残り%1$d分</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="other">残り%1$d秒</item> + </plurals> + <string name="title_domain_mutes">非表示のドメイン</string> + <string name="action_view_domain_mutes">非表示のドメイン</string> + <string name="action_mute_domain">%1$sをミュート</string> + <string name="pref_title_notification_filter_poll">投票の集計が完了した</string> + <string name="notification_poll_name">投票</string> + <string name="notification_poll_description">投票の集計が完了したときの通知</string> + <string name="pref_title_thread_filter_keywords">会話</string> + <string name="caption_notoemoji">Googleの現在の絵文字セットです</string> + <string name="action_add_poll">投票</string> + <string name="action_mentions">返信</string> + <string name="title_mentions_dialog">返信</string> + <string name="dialog_redraft_post_warning">この投稿を削除し、下書きに戻しますか?</string> + <string name="filter_dialog_remove_button">削除</string> + <string name="filter_dialog_update_button">更新</string> + <string name="error_create_list">リストを作成できませんでした</string> + <string name="error_delete_list">リストを削除できませんでした</string> + <string name="action_create_list">リストの作成</string> + <string name="action_delete_list">リストの削除</string> + <string name="hint_search_people_list">フォロワーを検索</string> + <string name="action_add_to_list">リストにアカウントを追加</string> + <string name="action_remove_from_list">リストからアカウントを削除</string> + <string name="poll_ended_voted">参加した投票の結果がでました</string> + <string name="poll_ended_created">作成した投票の結果がでました</string> + <string name="create_poll_title">投票</string> + <string name="duration_5_min">5分</string> + <string name="duration_30_min">30分</string> + <string name="duration_1_hour">1時間</string> + <string name="duration_6_hours">6時間</string> + <string name="duration_1_day">1日</string> + <string name="duration_3_days">3日</string> + <string name="duration_7_days">7日</string> + <string name="add_poll_choice">選択肢を追加</string> + <string name="poll_allow_multiple_choices">複数選択可</string> + <string name="edit_poll">編集</string> + <string name="pref_title_bot_overlay">ボットマークを表示</string> + <string name="pref_title_animate_gif_avatars">GIFアバターを動かす</string> + <string name="pref_title_public_filter_keywords">公開タイムライン</string> + <string name="description_post_cw">閲覧注意:%1$s</string> + <string name="filter_apply">適用</string> + <string name="poll_info_closed">投票終了</string> + <string name="title_accounts">アカウント</string> + <string name="failed_search">検索に失敗しました</string> + <string name="action_reset_schedule">リセット</string> + <string name="title_bookmarks">ブックマーク</string> + <string name="action_bookmark">ブックマーク</string> + <string name="action_edit">編集</string> + <string name="action_view_bookmarks">ブックマーク</string> + <string name="title_scheduled_posts">予約投稿</string> + <string name="action_access_scheduled_posts">予約投稿</string> + <string name="action_schedule_post">予約投稿</string> + <string name="description_visibility_private">フォロワー</string> + <string name="conversation_2_recipients">%1$sさん、%2$sさん</string> + <string name="notification_follow_request_name">フォローリクエスト</string> + <string name="action_unmute_desc">%1$sさんのミュートを解除</string> + <string name="notification_follow_request_format">%1$sさんがあなたにフォローリクエストしました</string> + <string name="report_sent_success">\@%1$sさんを通報しました</string> + <string name="no_scheduled_posts">予約した投稿はありません。</string> + <string name="no_drafts">下書きはありません。</string> + <string name="poll_new_choice_hint">項目 %1$d</string> + <string name="report_description_remote_instance">このアカウントは外部のサーバーにあります。匿名化された通報の複製をそちらにも送信しますか?</string> + <string name="report_description_1">通報をサーバーのモデレーターに送信します。以下にこのアカウントを通報理由を入力できます:</string> + <string name="failed_fetch_posts">投稿を取得できませんでした</string> + <string name="button_done">完了</string> + <string name="button_back">戻る</string> + <string name="button_continue">続ける</string> + <string name="compose_shortcut_short_label">投稿</string> + <string name="compose_shortcut_long_label">投稿する</string> + <string name="list">リスト</string> + <string name="hashtags">ハッシュタグ</string> + <string name="add_hashtag_title">ハッシュタグを追加</string> + <string name="description_post_media_no_description_placeholder">説明なし</string> + <string name="description_post_media">メディア: %1$s</string> + <string name="conversation_more_recipients">%1$sさん、%2$sさんと他%3$d人</string> + <string name="conversation_1_recipients">%1$sさん</string> + <string name="filter_add_description">フィルターする語句</string> + <string name="filter_dialog_whole_word_description">キーワードまたは語句が英数字のみである時、単語全体と一致する場合のみ適用されます</string> + <string name="notification_follow_request_description">フォローリクエストについての通知</string> + <string name="pref_main_nav_position_option_bottom">下部</string> + <string name="pref_main_nav_position_option_top">上部</string> + <string name="pref_main_nav_position">メインナビゲーションの位置</string> + <string name="pref_title_gradient_for_media">非表示のメディアを色付きのぼかしで表示する</string> + <string name="pref_title_notification_filter_follow_requests">フォローリクエスト</string> + <string name="dialog_mute_hide_notifications">通知を非表示</string> + <string name="dialog_mute_warning">\@%1$sさんをミュートしますか?</string> + <string name="dialog_block_warning">\@%1$sさんをブロックしますか?</string> + <string name="pref_title_alway_open_spoiler">閲覧注意としてマークされた投稿を常に展開する</string> + <string name="action_open_media_n">メディア #%1$d を開く</string> + <string name="action_open_faved_by">お気に入りを表示</string> + <string name="action_open_reblogger">ブーストしたユーザーを開く</string> + <string name="action_unmute_conversation">会話のミュートを解除</string> + <string name="action_mute_conversation">会話をミュート</string> + <string name="action_unmute_domain">%1$sのミュートを解除</string> + <string name="pref_title_enable_swipe_for_tabs">タブ間の切り替えにスワイプのジェスチャーを有効化</string> + <string name="pref_title_show_cards_in_timelines">リンクのプレビューをタイムラインに表示</string> + <string name="pref_title_confirm_reblogs">ブーストする前に確認を表示</string> + <string name="edit_hashtag_hint">ハッシュタグ (#をつけない)</string> + <string name="select_list_title">リストを選択</string> + <string name="poll_info_format"> <!-- 15 votes • 1 hour left --> %1$s • %2$s</string> + <string name="poll_info_time_absolute">%1$sに終了</string> + <string name="hint_additional_info">追加コメント</string> + <string name="report_remote_instance">%1$sに転送</string> + <string name="failed_report">通報に失敗しました</string> + <plurals name="poll_info_people"> + <item quantity="other">%1$s人</item> + </plurals> + <string name="warning_scheduling_interval">Mastodonにおける予約までの最小間隔は5分です。</string> + <string name="notification_subscription_format">%1$sさんが投稿しました</string> + <string name="title_announcements">お知らせ</string> + <string name="mute_domain_warning">本当に %1$s のすべてをブロックするのですか? そのドメインからのコンテンツは、公開タイムラインや通知に表示されなくなります。また、そのドメインのフォロワーは削除されます。</string> + <string name="post_media_audio">音声</string> + <string name="mute_domain_warning_dialog_ok">ドメイン全体を非表示</string> + <string name="about_powered_by_tusky">Tuskyによって提供されています</string> + <string name="delete_scheduled_post_warning">この予約投稿を削除しますか?</string> + <string name="instance_rule_info">ログインにより %1$s のルールに同意することになります。</string> + <string name="action_unbookmark">ブックマークを削除</string> + <string name="action_delete_conversation">会話を削除</string> + <string name="action_details">詳細情報</string> + <string name="notification_sign_up_format">%1$sさんがサインアップしました</string> + <string name="action_add_or_remove_from_list">リストへの追加または削除</string> + <string name="description_post_language">投稿言語</string> + <string name="description_post_favourited">お気に入りに追加しました</string> + <string name="description_post_bookmarked">ブックマークに追加しました</string> + <string name="label_duration">期間</string> + <string name="duration_indefinite">無期限</string> + <string name="duration_14_days">14日間</string> + <string name="duration_30_days">30日間</string> + <string name="duration_60_days">60日間</string> + <string name="no_lists">まだリストがありません。</string> + <string name="pref_title_show_self_username">ツールバーにユーザー名を表示する</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="compose_save_draft_loses_media">下書きを保存しますか? (添付ファイルは下書きの復元時に再アップロードされます。)</string> + <string name="duration_90_days">90日間</string> + <string name="duration_180_days">180日間</string> + <string name="duration_365_days">365日間</string> + <string name="limit_notifications">タイムライン通知を制限する</string> + <string name="wellbeing_hide_stats_posts">投稿上の数値的な統計情報を隠す</string> + <string name="failed_to_pin">ピン留めに失敗しました</string> + <string name="failed_to_unpin">ピン留めの解除に失敗しました</string> + <string name="description_post_reblogged">リブログしました</string> + <string name="saving_draft">下書きの保存中…</string> + <string name="pref_default_post_language">デフォルトの投稿言語</string> + <string name="pref_show_self_username_always">常に表示</string> + <string name="pref_show_self_username_disambiguate">複数アカウントでのログイン時</string> + <string name="pref_show_self_username_never">表示しない</string> + <string name="notification_subscription_name">新しい投稿</string> + <string name="notification_subscription_description">あなたが購読した誰かが新しい投稿をしたときの通知</string> + <string name="filter_dialog_whole_word">単語全体</string> + <string name="compose_preview_image_description">画像 %1$s に対する操作</string> + <string name="duration_no_change">(変更なし)</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="error_multimedia_size_limit">ビデオと音声ファイルのサイズは %1$s MB を超えることはできません。</string> + <string name="error_image_edit_failed">画像が編集できませんでした。</string> + <string name="title_login">ログイン</string> + <string name="action_edit_image">画像の編集</string> + <string name="instance_rule_title">%1$s のルール</string> + <string name="error_following_hashtags_unsupported">このインスタンスはハッシュタグのフォローに対応していません。</string> + <string name="title_followed_hashtags">フォローしたハッシュタグ</string> + <string name="confirmation_domain_unmuted">%1$s のミュートが解除されました</string> + <string name="dialog_push_notification_migration_other_accounts">プッシュ通知の購読許可を Tusky に与えるための現在のアカウントへの再ログインが完了しました。しかし、まだ他のアカウントはマイグレーションができていません。UnifiedPush 通知w有効にするには、他のアカウントにも切り替えて、それぞれ再ログインをしてください。</string> + <string name="action_unfollow_hashtag_format">#%1$s をフォロー解除しますか?</string> + <string name="notification_sign_up_description">新規ユーザーに関する通知</string> + <string name="notification_update_name">投稿の編集</string> + <string name="notification_report_format">%1$s に関する新しい報告</string> + <string name="notification_update_format">%1$sさんが投稿を編集しました</string> + <string name="post_edited">%1$s を編集しました</string> + <string name="notification_header_report_format">%1$sさんが %2$s を報告しました</string> + <string name="action_dismiss">キャンセル</string> + <string name="confirmation_hashtag_unfollowed">#%1$s をフォロー解除しました</string> + <string name="pref_title_animate_custom_emojis">カスタム絵文字のアニメーションを有効化</string> + <string name="dialog_delete_conversation_warning">この会話を削除しますか?</string> + <string name="pref_title_notification_filter_reports">新しい報告があります</string> + <string name="pref_title_notification_filter_subscriptions">購読している誰かが新しい投稿を公開しました</string> + <string name="pref_title_notification_filter_sign_ups">誰かがサインアップしました</string> + <string name="notification_sign_up_name">サインアップ</string> + <string name="notification_report_name">報告</string> + <string name="notification_report_description">モデレーション報告に関する通知</string> + <string name="post_media_attachments">添付ファイル</string> + <string name="status_count_one_plus">1+</string> + <string name="failed_to_add_to_list">アカウントのリストへの追加に失敗しました</string> + <string name="failed_to_remove_from_list">アカウントのリストからの削除に失敗しました</string> + <string name="no_announcements">アナウンスはありません。</string> + <string name="description_post_edited">編集しました</string> + <string name="pref_title_confirm_favourites">お気に入りに追加する前に確認を表示する</string> + <string name="pref_title_hide_top_toolbar">トップバーのタイトルを隠す</string> + <string name="pref_title_wellbeing_mode">ウェルビーイング</string> + <string name="account_note_hint">このアカウントに関するプライベートメモ</string> + <string name="account_note_saved">保存しました!</string> + <string name="wellbeing_hide_stats_profile">プロフィール上の数値的な統計情報を隠す</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="other">%1$d 個以上のメディア添付ファイルはアップロードできません。</item> + </plurals> + <string name="dialog_delete_list_warning">リスト %1$s を本当に削除しますか?</string> + <string name="drafts_post_failed_to_send">この投稿の送信に失敗しました!</string> + <string name="drafts_failed_loading_reply">返信情報の読み込みに失敗しました</string> + <string name="draft_deleted">下書きを削除しました</string> + <string name="drafts_post_reply_removed">下書きの返信先の投稿が削除されました</string> + <string name="action_subscribe_account">購読する</string> + <string name="wellbeing_mode_notice">あなたのメンタルヘルスに影響を与える可能性のある情報は隠されます。例: +\n +\n - お気に入り/ブースト/フォローの通知 +\n - 投稿のお気に入り/ブーストの数 +\n - プロフィールのフォロワー/投稿の統計情報 +\n +\n プッシュ通知には影響ありませんが、通知設定は手動で確認できます。</string> + <string name="review_notifications">通知の確認</string> + <string name="action_unsubscribe_account">購読の解除</string> + <string name="tips_push_notification_migration">プッシュ通知のサポートを有効にするにはすべてのアカウントで再ログインしてください。</string> + <string name="report_category_violation">ルール違反</string> + <string name="report_category_spam">スパム</string> + <string name="report_category_other">その他</string> + <string name="tusky_compose_post_quicksetting_label">投稿する</string> + <string name="dialog_push_notification_migration">UnifiedPush 経由でプッシュ通知を使用するには、Tusky に Mastodon サーバー上での購読許可が必要です。そのため、Tusky に与えられた OAuth スコープを変更するための再ログインが必要になります。ここかアカウント設定で再ログインのオプションを選んでも、ローカルの下書きとキャッシュは保存されます。</string> + <string name="error_following_hashtag_format">#%1$s のフォローエラー</string> + <string name="title_migration_relogin">プッシュ通知受け取るには再ログインしてください</string> + <string name="error_unfollowing_hashtag_format">#%1$s のフォロー解除エラー</string> + <string name="error_could_not_load_login_page">ログインページが読み込めませんでした。</string> + <string name="error_loading_account_details">アカウントの詳細情報の読み込みに失敗しました</string> + <string name="hint_media_description_missing">メディアには説明文が必要です。</string> + <string name="title_edits">編集</string> + <string name="post_media_alt">ALT</string> + <string name="error_muting_hashtag_format">#%1$s のミュート中にエラーが起こりました</string> + <string name="error_unmuting_hashtag_format">#%1$s のミュート解除中にエラーが起こりました</string> + <string name="action_add_reaction">リアクションを追加</string> + <string name="action_discard">変更を破棄</string> + <string name="action_continue_edit">編集を続ける</string> + <string name="action_share_account_link">アカウントへのリンクを共有</string> + <string name="action_share_account_username">アカウントのユーザー名を共有</string> + <string name="send_account_link_to">アカウント URL を共有…</string> + <string name="send_account_username_to">アカウントのユーザー名を共有…</string> + <string name="account_username_copied">ユーザー名がコピーされました</string> + <string name="a11y_label_loading_thread">スレッドの読み込み中</string> + <string name="pref_reading_order_newest_first">新しい順</string> + <string name="pref_title_reading_order">読む順番</string> + <string name="pref_reading_order_oldest_first">古い順</string> + <string name="set_focus_description">サムネイル画像で常に表示される中心点を設定するには、円をタップまたはドラッグして中してくだだい。</string> + <string name="mute_notifications_switch">通知のミュート</string> + <string name="account_date_joined">%1$s に参加</string> + <string name="status_edit_info">%1$s 編集</string> + <string name="status_created_info">%1$s の投稿</string> + <string name="post_lookup_error_format">投稿 %1$s の検索エラー</string> + <string name="follow_requests_info">アカウントがロックされていなかったとしても、%1$s のスタッフは以下のアカウントのフォローリクエストを確認した方がいいと判断しました。</string> + <string name="action_set_focus">中心点の設定</string> + <string name="compose_unsaved_changes">保存していない変更があります。</string> + <string name="error_status_source_load">サーバーからステータスの元情報を取得できませんでした。</string> + <string name="pref_summary_http_proxy_disabled">無効</string> + <string name="pref_summary_http_proxy_missing"><設定なし></string> + <string name="pref_summary_http_proxy_invalid"><無効></string> + <string name="pref_title_notification_filter_updates">やり取りした投稿が編集された時</string> + <string name="notification_update_description">やり取りした投稿が編集されたときの通知</string> + <string name="pref_title_http_proxy_port_message">ポートは %1$d から %2$d の間でなければなりません</string> + <string name="action_post_failed">アップロードに失敗しました</string> + <string name="action_post_failed_detail">アップロードに失敗した投稿は下書きに保存されました。 +\n +\nサーバーと接続できなかったか、投稿が拒否されました。</string> + <string name="action_post_failed_show_drafts">下書きを表示</string> + <string name="action_post_failed_do_nothing">閉じる</string> + <string name="action_browser_login">ブラウザでログイン</string> + <string name="description_login">ほとんどの場合に動作します。他のアプリにはデータが漏洩しません。</string> + <string name="description_browser_login">追加の認証方法がサポートされる可能性がありますが、対応ブラウザが必要です。</string> + <string name="action_post_failed_detail_plural">アップロードに失敗した投稿は下書きに保存されました。 +\n +\nサーバーと接続できなかったか、投稿が拒否されました。</string> + <string name="dialog_follow_hashtag_title">ハッシュタグをフォロー</string> + <string name="dialog_follow_hashtag_hint">#ハッシュタグ</string> + <string name="post_media_image">画像</string> + <string name="notification_unknown_name">不明</string> + <string name="select_list_manage">リストを管理する</string> + <string name="status_filtered_show_anyway">とにかく表示する</string> + <string name="pref_title_account_filter_keywords">プロフィール</string> + <string name="title_public_trending_hashtags">トレンドのハッシュタグ</string> + <string name="pref_title_show_stat_inline">タイムラインに投稿の統計情報を表示する</string> + <string name="pref_ui_text_size">UI のテキストサイズ</string> + <string name="notification_listenable_worker_name">バックグラウンドアクティビティ</string> + <string name="notification_listenable_worker_description">Tusky がバックグラウンドで動作中の通知</string> + <string name="notification_notification_worker">通知を取得中…</string> + <string name="notification_prune_cache">キャッシュの整理中…</string> + <string name="label_filter_context">フィルターコンテキスト</string> + <string name="filter_action_warn">警告</string> + <string name="label_filter_keywords">フィルターするキーワードまたはフレーズ</string> + <string name="filter_action_hide">非表示</string> + <string name="filter_description_warn">警告付きで隠す</string> + <string name="filter_description_hide">完全に隠す</string> + <string name="label_filter_action">フィルターアクション</string> + <string name="action_refresh">再読み込み</string> + <string name="ui_error_reblog">投稿のブーストに失敗しました: %1$s</string> + <string name="ui_error_clear_notifications">通知の消去に失敗しました: %1$s</string> + <string name="ui_error_favourite">投稿のお気に入りに失敗しました: %1$s</string> + <string name="ui_error_bookmark">投稿のブックマークに失敗しました: %1$s</string> + <string name="ui_success_rejected_follow_request">フォローリクエストがブロックされました</string> + <string name="ui_success_accepted_follow_request">フォローリクエストが許可されました</string> + <string name="ui_error_vote">投票に失敗しました: %1$s</string> + <string name="action_add">追加</string> + <string name="filter_keyword_display_format">%1$s (単語全体)</string> + <string name="filter_keyword_addition_title">キーワードを追加</string> + <string name="total_usage">合計使用量</string> + <string name="total_accounts">総アカウント</string> + <string name="ui_error_unknown">不明な理由</string> + <string name="ui_error_accept_follow_request">フォローリクエストの承認に失敗しました: %1$s</string> + <string name="ui_error_reject_follow_request">フォローリクエストの拒否に失敗しました: %1$s</string> + <string name="hint_filter_title">フィルター</string> + <string name="label_filter_title">タイトル</string> + <string name="filter_edit_keyword_title">キーワードを編集</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="load_newest_notifications">最新の通知を読み込む</string> + <string name="compose_delete_draft">下書きを削除しますか?</string> + <string name="error_media_upload_sending_fmt">アップロードに失敗しました: %1$s</string> + <string name="error_missing_edits">あなたのサーバーは、この投稿が変更されたことを把握していますが、編集履歴のコピーを備えていないので、表示できません。 +\n +\nこれは<a href="https://github.com/mastodon/mastodon/issues/25398">Mastodonのissue #25398</a>です。</string> + <string name="title_public_trending_statuses">トレンドの投稿</string> + <string name="about_device_info">%1$s %2$s +\nAndroid バージョン: %3$s +\nSDK バージョン: %4$d</string> + <string name="about_account_info_title">あなたのアカウント</string> + <string name="about_device_info_title">あなたのデバイス</string> + <string name="about_account_info">\@%1$s@%2$s +\nバージョン: %3$s</string> + <string name="about_copy">バージョンとデバイス情報をコピー</string> + <string name="about_copied">バージョンとデバイス情報をコピーしました</string> + <string name="error_media_playback">再生に失敗しました: \t%1$s</string> + <string name="dialog_delete_filter_text">フィルタ「%1$s」を削除しますか?</string> + <string name="dialog_delete_filter_positive_action">削除する</string> + <string name="dialog_save_profile_changes_message">プロファイルの変更を保存しますか?</string> + <string name="app_theme_system_black">システムのデザインを使用 (ブラック)</string> + <string name="list_exclusive_label">ホームタイムラインで隠す</string> + <string name="notification_summary_report_format">%1$s · %2$d 個の投稿を添付</string> + <string name="unmuting_hashtag_success_format">ハッシュタグ #%1$s のミュートを解除</string> + <string name="action_view_filter">フィルタを表示</string> + <string name="status_filter_placeholder_label_format">フィルタ済み: %1$s</string> + <string name="error_unblocking_domain">%1$s のミュート解除に失敗しました: %2$s</string> + <string name="error_blocking_domain">%1$s のミュートに失敗しました: %2$s</string> + <string name="label_image">画像</string> + <string name="list_reply_policy_list">リストのメンバー</string> + <string name="pref_title_show_self_boosts">自己ブーストを表示</string> + <string name="pref_title_show_self_boosts_description">自分自身の投稿をブーストしている誰か</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-kab/strings.xml b/app/src/main/res/values-kab/strings.xml new file mode 100644 index 0000000..af6ba7f --- /dev/null +++ b/app/src/main/res/values-kab/strings.xml @@ -0,0 +1,275 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="action_login">Qqen ɣer Maṣṭudun</string> + <string name="title_favourites">Ismenyifen</string> + <string name="title_drafts">Irewwayen</string> + <string name="action_logout">Ffeɣ</string> + <string name="action_view_preferences">Iɣewwaṛen</string> + <string name="action_view_account_preferences">Iɣewwaṛen n umiḍan</string> + <string name="action_edit_profile">Ẓreg amaɣnu</string> + <string name="action_search">Nadi</string> + <string name="about_title_activity">Ɣef</string> + <string name="action_lists">Tabdart</string> + <string name="title_lists">Tabdarin</string> + <string name="error_compose_character_limit">Izen-ik·im aṭas i ɣezzif!</string> + <string name="title_home">Agejdan</string> + <string name="title_tab_preferences">Iccaren</string> + <string name="title_view_thread">Sensla</string> + <string name="title_posts">Iznan</string> + <string name="title_posts_with_replies">S tririyin</string> + <string name="title_edit_profile">Ẓreg amaɣnu-ik</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_content_warning_show_more">Zeṛ ugar</string> + <string name="post_content_warning_show_less">Zeṛ kra kan</string> + <string name="message_empty">Ulac walu da.</string> + <string name="notification_follow_format">%1$s yeṭṭafar-ik·ikem-id</string> + <string name="action_reply">Err</string> + <string name="action_more">Ugar</string> + <string name="action_compose">Azen</string> + <string name="action_follow">Ḍfeṛ</string> + <string name="action_unfollow">Ur ṭṭafar ara</string> + <string name="action_edit">Ẓreg</string> + <string name="action_delete">Kkes</string> + <string name="action_delete_and_redraft">Kkes tɛiwdeḍ tira</string> + <string name="action_send">Jewweq</string> + <string name="action_send_public">JEWWEQ!</string> + <string name="action_retry">Ɛreḍ tikkelt-nniḍen</string> + <string name="action_close">Mdel</string> + <string name="action_view_profile">Amaɣnu</string> + <string name="action_view_favourites">Ismenyifen</string> + <string name="action_open_in_web">Ldi deg uminig</string> + <string name="action_share">Bḍu</string> + <string name="action_mute">Sgugem</string> + <string name="action_access_drafts">Irewwayen</string> + <string name="action_open_faved_by">Sken-d ismenyifen</string> + <string name="notification_favourite_name">Ismenyifen</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> n usmenyaf</item> + <item quantity="other"><b>%1$s</b> n ismenyifen</item> + </plurals> + <string name="no_drafts">Ur tesɛiḍ ara irewwayen.</string> + <string name="error_generic">Tella-d tucḍa.</string> + <string name="title_notifications">Tilɣa</string> + <string name="link_whats_an_instance">D acu i ttummant\?</string> + <string name="title_bookmarks">Ticraḍ</string> + <string name="action_bookmark">Rnu ɣer ticraḍ</string> + <string name="action_view_bookmarks">Ticraḍ</string> + <string name="action_mute_domain">Sgugem %1$s</string> + <string name="action_mention">Bder</string> + <string name="action_save">Sekles</string> + <string name="action_edit_own_profile">Ẓreg</string> + <string name="action_undo">Sefsex</string> + <string name="action_emoji_keyboard">Anasiw n imujiyen</string> + <string name="action_add_tab">Rnu iccer</string> + <string name="action_copy_link">Nɣel aseɣwen</string> + <string name="action_open_as">Ldi amzun d %1$s</string> + <string name="action_share_as">Bḍu amzun d…</string> + <string name="send_post_link_to">Bḍu aseɣwen n tijewwiq s…</string> + <string name="send_post_content_to">Bḍu tijewwiqt d…</string> + <string name="hint_domain">Anta tummant\?</string> + <string name="hint_compose">d-acu i gellan d amaynut\?</string> + <string name="hint_search">Nadi…</string> + <string name="label_quick_reply">Tiririn…</string> + <string name="label_avatar">Tugna n umaɣnu</string> + <string name="dialog_download_image">Sider</string> + <string name="dialog_delete_post_warning">Kkes tijewwiqt-a\?</string> + <string name="pref_title_edit_notification_settings">Ẓreg tilɣa</string> + <string name="pref_title_appearance_settings">Agrudem</string> + <string name="app_theme_light">Aceɛlal</string> + <string name="app_theme_black">Aberkan</string> + <string name="pref_title_language">Tutlayt</string> + <string name="pref_title_post_tabs">Iccaren</string> + <string name="pref_title_proxy_settings">Apṛuksi</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s d %4$d nniḍen</string> + <string name="notification_summary_medium">%1$s, %2$s, akked %3$s</string> + <string name="notification_summary_small">%1$s akked %2$s</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_account">Amaɣnu n Tusky</string> + <string name="post_media_images">Tugniwin</string> + <string name="post_media_video">Tibidyutin</string> + <string name="follows_you">Yeṭṭafar-ik·ikem-id</string> + <string name="filter_dialog_remove_button">Kkes</string> + <string name="filter_dialog_update_button">Lqem</string> + <string name="add_account_name">Rnu amiḍan</string> + <string name="add_account_description">Rnu yiwen umiḍan amaynut n Maṣṭudun</string> + <string name="action_compose_shortcut">Aru</string> + <string name="error_no_custom_emojis">Tummant-ik·im %1$s ur tesɛi ara imujiyen udmawanen</string> + <string name="action_open_post">Ldi tijewwiqt</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="pin_action">Senṭeḍ</string> + <string name="action_view_mutes">Imiḍanen yettwasgugmen</string> + <string name="action_view_blocks">Imiḍan yettusḥebsen</string> + <string name="action_view_domain_mutes">Tiɣula yettwaffren</string> + <string name="action_view_follow_requests">Isuturen n teḍfeṛt</string> + <string name="action_view_media">Taɣwalt</string> + <string name="notifications_clear">Sfeḍ</string> + <string name="title_mutes">Imiḍanen yettwasgugmen</string> + <string name="title_blocks">Imiḍan yettusḥebsen</string> + <string name="title_domain_mutes">Tiɣula yettwaffren</string> + <string name="title_follow_requests">Isuturen n teḍfeṛt</string> + <string name="pref_title_notifications_enabled">Tilɣa</string> + <string name="title_media">Taɣwalt</string> + <string name="action_remove">Kkes</string> + <string name="compose_shortcut_short_label">Azen</string> + <string name="edit_poll">Ẓreg</string> + <string name="title_posts_pinned">Yettwanṭḍen</string> + <string name="action_add_media">Rnu amidya</string> + <string name="action_add_poll">Rnu assenqed</string> + <string name="action_photo_take">Ṭef tugna</string> + <string name="action_toggle_visibility">Timeẓriwt n yizen</string> + <string name="action_schedule_post">Sɣiwes tijewwaqt-a</string> + <string name="post_share_content">Bḍu agbur n tijewwiqt-a</string> + <string name="post_share_link">Bḍu aseɣwen ɣer tijewwiqt</string> + <string name="filter_addition_title">Rnu amsizdeg</string> + <string name="filter_edit_title">Ẓreg amsizdeg</string> + <string name="action_create_list">Snulfu-d tabdart</string> + <string name="action_rename_list">Snifel isem n tabdart</string> + <string name="action_delete_list">Kkes tabdart-a</string> + <string name="action_add_to_list">Rnu yiwen umiḍan ɣer tabdart</string> + <string name="action_remove_from_list">Kkes amiḍan seg tabdart</string> + <string name="profile_metadata_add">Rnu isefka</string> + <string name="hint_list_name">Isem n tebdart</string> + <string name="select_list_title">Fren tabdart</string> + <string name="list">Tabdart</string> + <string name="notifications_apply_filter">Sizdeg</string> + <string name="title_accounts">Imiḍanen</string> + <string name="add_poll_choice">Rnu yiwen wefran</string> + <string name="report_username_format">Cetki ɣef @%1$s</string> + <string name="action_report">Cetki fell-as</string> + <string name="action_reject">Ggami</string> + <string name="download_image">Yessidired %1$s</string> + <string name="send_media_to">Bḍu tugna s…</string> + <string name="login_connection">itteqqen…</string> + <string name="dialog_message_uploading_media">Issalay…</string> + <string name="pref_title_notification_filter_poll">fukken kran n wadɣaren</string> + <string name="pref_title_timeline_filters">Imzizdigen</string> + <string name="app_theme_auto">Awurman akken yella yiṭij</string> + <string name="pref_title_browser_settings">Iminig</string> + <string name="pref_title_show_replies">Sken-d tiririyin</string> + <string name="pref_title_http_proxy_settings">Apṛuksi HTTP</string> + <string name="pref_title_http_proxy_server">Tansa n upṛuksi HTTP</string> + <string name="notification_follow_name">Imeḍfaṛen imaynuten</string> + <string name="notification_poll_name">Adɣaren</string> + <string name="notification_mention_format">Yuder-ik-id %1$s</string> + <string name="description_account_locked">Yettwargel umiḍan</string> + <string name="abbreviated_years_ago">%1$dis aya</string> + <string name="abbreviated_days_ago">%1$dus aya</string> + <string name="replying_to">Tettaraḍ-as i @%1$s</string> + <string name="load_more_placeholder_text">awid ugar</string> + <string name="pref_title_thread_filter_keywords">Idewenniyen</string> + <string name="lock_account_label">Rgel amiḍan</string> + <string name="performing_lookup_title">Yettnadi…</string> + <string name="restart">Ales tanekra</string> + <string name="download_failed">Tuccḍa n usider</string> + <string name="account_moved_description">Igujj %1$s ɣer:</string> + <string name="unpin_action">Kkes asenṭeḍ</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s akked %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s akked %3$d nniḍen</string> + <string name="compose_shortcut_long_label">Aru izen</string> + <string name="poll_info_format"> <!-- 15 n wadɣaren • 1 n wesrag id yeqqimen --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s n wedɣar</item> + <item quantity="other">%1$s n yedɣaren</item> + </plurals> + <string name="poll_info_time_absolute">ad ifak deg %1$s</string> + <string name="poll_info_closed">ifuk</string> + <string name="poll_vote">Dɣer</string> + <string name="poll_ended_voted">Ifuk, tura kan, yiwen wedɣar t tteki-iḍ degs</string> + <string name="poll_ended_created">Ifukk yiwen wedɣar id snulfaḍ</string> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d n wass id yugran</item> + <item quantity="other">%1$d n wussan id yugran</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d wesrag id yugran</item> + <item quantity="other">%1$d n yisragen id yugran</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d n tasdidt id yugran</item> + <item quantity="other">%1$d n tisdidin id yugran</item> + </plurals> + <string name="button_continue">Kemmel</string> + <string name="button_back">Uɣal</string> + <string name="failed_report">Tella-d tuccḍa deg cetki</string> + <string name="failed_search">Tucḍa n unadi</string> + <string name="create_poll_title">Assenqed</string> + <string name="duration_5_min">5 n tisdidin</string> + <string name="duration_30_min">30 n tisdidin</string> + <string name="duration_1_hour">1 n wesrag</string> + <string name="duration_6_hours">6 n yisragen</string> + <string name="duration_1_day">1 n wass</string> + <string name="duration_3_days">3 n wussan</string> + <string name="duration_7_days">7 n wussan</string> + <string name="poll_new_choice_hint">Tafrant %1$d</string> + <string name="title_follows">Ig ṭṭafar</string> + <string name="title_followers">Imeḍfaṛen</string> + <string name="hint_search_people_list">Nadi ɣef medden ar at ḍfereḍ</string> + <string name="description_visibility_private">Imeḍfaṛen</string> + <string name="action_links">Iseɣwan</string> + <string name="action_mentions">Tibdarin</string> + <string name="title_mentions_dialog">Tibdarin</string> + <string name="title_links_dialog">Iseɣwan</string> + <string name="confirmation_reported">Yettwaceyyeɛ!</string> + <string name="search_no_results">Ula d yiwen n ugmuḍ</string> + <string name="post_privacy_followers_only">I yimeḍfaṛen kan</string> + <string name="pref_post_text_size">Teɣzi n weḍṛis</string> + <string name="about_powered_by_tusky">Yettwamdemmar s Tusky</string> + <string name="about_project_site">Asmel Web n usenfaṛ: +\n https://tusky.app</string> + <string name="abbreviated_hours_ago">%1$dsr</string> + <string name="abbreviated_minutes_ago">%1$dtsd</string> + <string name="abbreviated_seconds_ago">%1$dtsn</string> + <string name="compose_save_draft">Sekles amzun d arewway\?</string> + <string name="later">Ticki</string> + <string name="profile_badge_bot_text">Aṛubut</string> + <string name="description_post_bookmarked">Yettwarna ɣer ticṛad</string> + <string name="abbreviated_in_hours">deg %1$dsr</string> + <string name="abbreviated_in_minutes">deg %1$dtsd</string> + <string name="abbreviated_in_seconds">deg %1$dtsn</string> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d n tasint id yugran</item> + <item quantity="other">%1$d n tasinin id yugran</item> + </plurals> + <string name="post_sensitive_media_title">Agbur amḥulfu</string> + <string name="pref_default_media_sensitivity">Creḍ allal n teywalt amzun d amḥulfu</string> + <string name="action_reset_schedule">Wennez tikkelt-nniḍen</string> + <string name="error_media_upload_sending">Asali ur yeddi ara.</string> + <string name="error_sender_account_gone">Tuccḍa deg tuzna n tijewwiqt.</string> + <string name="title_public_local">Adigan</string> + <string name="title_licenses">Turagin</string> + <string name="post_boosted_format">Yebḍa-t %1$s</string> + <string name="notification_reblog_format">%1$s yebḍa izen-ik·im</string> + <string name="notification_favourite_format">%1$s yerna izen-ik·im ɣer ismenyafen-is</string> + <string name="action_quick_reply">Tiririt taruradt</string> + <string name="action_reblog">Bḍu</string> + <string name="action_unreblog">Kkes beṭu</string> + <string name="action_hide_reblogs">Ffer beṭuyat</string> + <string name="action_show_reblogs">Sken-d beṭuyat</string> + <string name="action_unmute">Ur sgugum ara</string> + <string name="action_accept">Ddeg</string> + <string name="action_hashtags">Ihacṭagen</string> + <string name="action_open_reblogged_by">Sken-d beṭuyat</string> + <string name="title_hashtags_dialog">Ihacṭagen</string> + <string name="confirmation_unmuted">Aseqdac nni ur yettwasgugem ara tura</string> + <string name="confirmation_domain_unmuted">%1$s ur yettwaffer ara</string> + <string name="hint_note">Assisen</string> + <string name="label_header">Tugna n yiɣef n umaɣnu</string> + <string name="error_empty">Ur ilaq ara ad yili d ilem.</string> + <string name="action_block">Cekkel</string> + <string name="action_unblock">Kkes tacekkalt</string> + <string name="confirmation_unblocked">Tettwakkes tacekkalt ɣef umiḍan-nni</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s n wemdan</item> + <item quantity="other">%1$s n yemdanen</item> + </plurals> + <string name="report_remote_instance">Bren-it ɣer %1$s</string> + <string name="pref_title_app_theme">Asentel n wesnas</string> + <string name="no_scheduled_posts">Ulac ɣur-k·m ula d yiwet n tjewwiqt yettwasɣawsen.</string> + <string name="action_access_scheduled_posts">Tijewwiqin yettwasɣawsen</string> + <string name="title_scheduled_posts">Tijewwiqin yettwasɣawsen</string> + <string name="pref_title_show_boosts">Sken-d beṭuyat</string> + <string name="hashtags">Ihacṭagen</string> + <string name="notification_follow_request_name">Isuturen n teḍfeṛt</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml new file mode 100644 index 0000000..6733a90 --- /dev/null +++ b/app/src/main/res/values-ko/strings.xml @@ -0,0 +1,419 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">오류가 발생했습니다.</string> + <string name="error_network">네트워크 오류가 발생했습니다! 인터넷 연결을 확인하고 다시 시도하십시오!</string> + <string name="error_empty">내용을 입력하십시오.</string> + <string name="error_invalid_domain">도메인이 올바르지 않습니다</string> + <string name="error_failed_app_registration">해당 인스턴스에 대한 인증에 실패했습니다.</string> + <string name="error_no_web_browser_found">사용 가능한 웹 브라우저를 찾을 수 없습니다.</string> + <string name="error_authorization_unknown">알 수 없는 인증 오류가 발생했습니다.</string> + <string name="error_authorization_denied">인증이 거부되었습니다.</string> + <string name="error_retrieving_oauth_token">로그인 토큰을 받아올 수 없습니다.</string> + <string name="error_compose_character_limit">게시물 길이가 너무 깁니다!</string> + <string name="error_media_upload_type">이 파일은 첨부할 수 없습니다.</string> + <string name="error_media_upload_opening">이 파일을 읽지 못했습니다.</string> + <string name="error_media_upload_permission">미디어를 읽기 위한 권한이 필요합니다.</string> + <string name="error_media_download_permission">미디어를 저장하기 위한 권한이 필요합니다.</string> + <string name="error_media_upload_image_or_video">이미지와 비디오는 동시에 첨부할 수 없습니다.</string> + <string name="error_media_upload_sending">파일을 업로드하지 못했습니다.</string> + <string name="error_sender_account_gone">툿을 전송하지 못했습니다.</string> + <string name="title_home">홈</string> + <string name="title_notifications">알림</string> + <string name="title_public_local">로컬</string> + <string name="title_public_federated">연합</string> + <string name="title_direct_messages">다이렉트 메시지</string> + <string name="title_tab_preferences">탭</string> + <string name="title_view_thread">툿</string> + <string name="title_posts">툿</string> + <string name="title_posts_with_replies">툿과 답장</string> + <string name="title_posts_pinned">고정된 툿</string> + <string name="title_follows">팔로우</string> + <string name="title_followers">팔로워</string> + <string name="title_favourites">좋아요</string> + <string name="title_mutes">뮤트한 유저</string> + <string name="title_blocks">블록한 유저</string> + <string name="title_domain_mutes">숨긴 도메인</string> + <string name="title_follow_requests">팔로우 요청</string> + <string name="title_edit_profile">프로필 편집</string> + <string name="title_drafts">임시 저장</string> + <string name="title_licenses">라이선스</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s님이 부스트 했습니다</string> + <string name="post_sensitive_media_title">민감한 미디어</string> + <string name="post_media_hidden_title">미디어 숨겨짐</string> + <string name="post_sensitive_media_directions">클릭하여 보기</string> + <string name="post_content_warning_show_more">더 보기</string> + <string name="post_content_warning_show_less">숨기기</string> + <string name="post_content_show_more">더 보기</string> + <string name="post_content_show_less">줄이기</string> + <string name="message_empty">메시지가 없습니다.</string> + <string name="footer_empty">게시물이 없습니다. 아래로 당겨서 새로고침하세요!</string> + <string name="notification_reblog_format">%1$s님이 부스트 했습니다</string> + <string name="notification_favourite_format">%1$s님이 당신의 게시물을 좋아합니다</string> + <string name="notification_follow_format">%1$s님이 나를 팔로우 했습니다</string> + <string name="report_username_format">\@%1$s 신고</string> + <string name="report_comment_hint">코멘트</string> + <string name="action_quick_reply">빠른 답장</string> + <string name="action_reply">답장</string> + <string name="action_reblog">부스트</string> + <string name="action_unreblog">부스트 해제</string> + <string name="action_favourite">좋아요</string> + <string name="action_unfavourite">좋아요 취소</string> + <string name="action_more">더 보기</string> + <string name="action_compose">작성</string> + <string name="action_login">마스토돈에 로그인</string> + <string name="action_logout">로그아웃</string> + <string name="action_logout_confirm">정말로 %1$s에서 로그아웃하시겠습니까\?</string> + <string name="action_follow">팔로우</string> + <string name="action_unfollow">팔로우 해제</string> + <string name="action_block">차단</string> + <string name="action_unblock">차단 해제</string> + <string name="action_hide_reblogs">이 유저의 부스트 숨기기</string> + <string name="action_show_reblogs">부스트한 유저 보이기</string> + <string name="action_report">신고</string> + <string name="action_delete">삭제</string> + <string name="action_delete_and_redraft">지우고 다시 쓰기</string> + <string name="action_send">툿</string> + <string name="action_send_public">툿!</string> + <string name="action_retry">다시 시도</string> + <string name="action_close">닫기</string> + <string name="action_view_profile">프로필</string> + <string name="action_view_preferences">설정</string> + <string name="action_view_account_preferences">계정 설정</string> + <string name="action_view_favourites">좋아요</string> + <string name="action_view_mutes">뮤트한 유저</string> + <string name="action_view_blocks">블록한 유저</string> + <string name="action_view_domain_mutes">숨긴 도메인</string> + <string name="action_view_follow_requests">팔로우 요청</string> + <string name="action_view_media">미디어</string> + <string name="action_open_in_web">브라우저에서 열기</string> + <string name="action_add_media">미디어 추가</string> + <string name="action_photo_take">사진 촬영</string> + <string name="action_share">공유</string> + <string name="action_mute">뮤트</string> + <string name="action_unmute">뮤트 해제</string> + <string name="action_mute_domain">%1$s 전체를 숨김</string> + <string name="action_mention">멘션</string> + <string name="action_hide_media">미디어 숨기기</string> + <string name="action_open_drawer">메뉴 열기</string> + <string name="action_save">저장</string> + <string name="action_edit_profile">프로필 편집</string> + <string name="action_edit_own_profile">편집</string> + <string name="action_undo">실행 취소</string> + <string name="action_accept">수락</string> + <string name="action_reject">거절</string> + <string name="action_search">검색</string> + <string name="action_access_drafts">임시 저장</string> + <string name="action_toggle_visibility">공개 범위</string> + <string name="action_content_warning">열람 주의</string> + <string name="action_emoji_keyboard">이모지 추가</string> + <string name="action_add_tab">탭 추가</string> + <string name="action_links">링크</string> + <string name="action_mentions">멘션</string> + <string name="action_hashtags">해시태그</string> + <string name="action_open_reblogger">부스트한 유저의 프로필로 이동</string> + <string name="action_open_reblogged_by">부스트 보이기</string> + <string name="action_open_faved_by">좋아요한 유저 보이기</string> + <string name="title_hashtags_dialog">해시태그</string> + <string name="title_mentions_dialog">멘션</string> + <string name="title_links_dialog">링크</string> + <string name="action_open_media_n">미디어 #%1$d 열기</string> + <string name="download_image">%1$s 다운로드 중</string> + <string name="action_copy_link">링크 복사</string> + <string name="action_share_as">공유</string> + <string name="download_media">다운로드</string> + <string name="downloading_media">미디어 다운로드 중</string> + <string name="send_post_link_to">툿 URL 공유</string> + <string name="send_post_content_to">툿 공유</string> + <string name="send_media_to">미디어 공유</string> + <string name="confirmation_reported">신고를 보냈습니다!</string> + <string name="confirmation_unblocked">차단이 해제됨</string> + <string name="confirmation_unmuted">뮤트가 해제됨</string> + <string name="confirmation_domain_unmuted">%1$s 숨김 해제됨</string> + <string name="hint_domain">인스턴스 주소</string> + <string name="hint_compose">지금 무엇을 하고 있나요\?</string> + <string name="hint_content_warning">열람 주의</string> + <string name="hint_display_name">표시되는 이름</string> + <string name="hint_note">자기소개</string> + <string name="hint_search">검색...</string> + <string name="search_no_results">검색 결과가 없습니다</string> + <string name="label_quick_reply">답장</string> + <string name="label_avatar">아바타</string> + <string name="label_header">헤더</string> + <string name="link_whats_an_instance">인스턴스가 무엇인가요\?</string> + <string name="login_connection">연결 중...</string> + <string name="dialog_whats_an_instance">인스턴스의 도메인 주소나 IP주소를 입력하실 수 있습니다. mastodon.social, icosahedron.website, social.tchncs.de 등이 있으며, 그 외에도 <a href="https://instances.social">더 많은 인스턴스</a>가 당신을 기다리고 있습니다! +\n +\n만약 계정이 없으시다면, 인스턴스 주소를 입력하신 후에 계정을 만드실 수 있습니다. +\n +\n여러분이 어느 인스턴스에 가입하시더라도, 다른 인스턴스에 있는 유저들과 문제 없이 소통하실 수 있습니다. +\n +\n자세한 사항은 <a href="https://joinmastodon.org">joinmastodon.org</a>을 참조하세요. </string> + <string name="dialog_title_finishing_media_upload">미디어 업로드 완료</string> + <string name="dialog_message_uploading_media">업로드 중...</string> + <string name="dialog_download_image">다운로드</string> + <string name="dialog_message_cancel_follow_request">팔로우 요청을 취소하시겠습니까\?</string> + <string name="dialog_unfollow_warning">이 계정을 팔로우 해제하시겠습니까\?</string> + <string name="dialog_delete_post_warning">이 툿을 삭제하시겠습니까\?</string> + <string name="dialog_redraft_post_warning">이 툿을 지우고 다시 작성하시겠습니까\?</string> + <string name="mute_domain_warning">정말로 %1$s 전체를 숨기시겠습니까\? 모든 공개 타임라인과 알림에서 해당 도메인에서 작성된 컨텐츠를 보지 못합니다. 해당 도메인 팔로워와의 관계가 사라집니다.</string> + <string name="mute_domain_warning_dialog_ok">도메인 전체를 숨기기</string> + <string name="visibility_public">공개: 공개 타임라인에 표시</string> + <string name="visibility_unlisted">비표시: 공개 타임라인에 표시하지 않음</string> + <string name="visibility_private">비공개: 팔로워에게만 공개</string> + <string name="visibility_direct">다이렉트: 멘션한 사용자에게만 공개</string> + <string name="pref_title_edit_notification_settings">알림</string> + <string name="pref_title_notifications_enabled">알림</string> + <string name="pref_title_notification_alerts">알림</string> + <string name="pref_title_notification_alert_sound">소리</string> + <string name="pref_title_notification_alert_vibrate">진동</string> + <string name="pref_title_notification_alert_light">LED 상태표시등</string> + <string name="pref_title_notification_filters">알림</string> + <string name="pref_title_notification_filter_mentions">누군가가 나를 멘션했을때</string> + <string name="pref_title_notification_filter_follows">누군가가 나를 팔로우했을 때</string> + <string name="pref_title_notification_filter_reblogs">누군가가 내 게시물을 부스트했을 때</string> + <string name="pref_title_notification_filter_favourites">누군가가 내 게시물을 좋아요했을 때</string> + <string name="pref_title_notification_filter_poll">투표가 종료되었을 때</string> + <string name="pref_title_appearance_settings">화면</string> + <string name="pref_title_app_theme">어플리케이션 테마</string> + <string name="pref_title_timelines">타임라인</string> + <string name="pref_title_timeline_filters">필터</string> + <string name="app_them_dark">어두움</string> + <string name="app_theme_light">밝음</string> + <string name="app_theme_black">검정</string> + <string name="app_theme_auto">시간에 따라 자동으로 변경</string> + <string name="app_theme_system">시스템 기본값</string> + <string name="pref_title_browser_settings">웹 브라우저</string> + <string name="pref_title_custom_tabs">Chrome 커스텀 탭 사용</string> + <string name="pref_title_language">언어/Language</string> + <string name="pref_title_bot_overlay">봇 식별자 추가</string> + <string name="pref_title_animate_gif_avatars">GIF 아바타 애니메이션 보이기</string> + <string name="pref_title_post_filter">타임라인 필터링</string> + <string name="pref_title_post_tabs">탭</string> + <string name="pref_title_show_boosts">부스트 보이기</string> + <string name="pref_title_show_replies">답장 보이기</string> + <string name="pref_title_show_media_preview">미디어 미리보기 다운로드</string> + <string name="pref_title_proxy_settings">프록시</string> + <string name="pref_title_http_proxy_settings">HTTP 프록시</string> + <string name="pref_title_http_proxy_enable">HTTP 프록시 사용하기</string> + <string name="pref_title_http_proxy_server">HTTP 프록시 서버</string> + <string name="pref_title_http_proxy_port">HTTP 프록시 포트</string> + <string name="pref_default_post_privacy">기본 공개 범위</string> + <string name="pref_default_media_sensitivity">미디어를 항상 열람주의로 설정</string> + <string name="pref_publishing">게시물 작성 (서버와 동기화)</string> + <string name="pref_failed_to_sync">설정을 동기화하지 못했습니다</string> + <string name="post_privacy_public">공개</string> + <string name="post_privacy_unlisted">타임라인에 비표시</string> + <string name="post_privacy_followers_only">비공개</string> + <string name="pref_post_text_size">게시물 글자 크기</string> + <string name="post_text_size_smallest">매우 작게</string> + <string name="post_text_size_small">작게</string> + <string name="post_text_size_medium">보통</string> + <string name="post_text_size_large">크게</string> + <string name="post_text_size_largest">매우 크게</string> + <string name="notification_mention_name">멘션</string> + <string name="notification_mention_descriptions">누군가 나를 멘션할 때 알림</string> + <string name="notification_follow_name">팔로우</string> + <string name="notification_follow_description">누군가 나를 팔로우할 때 알림</string> + <string name="notification_boost_name">부스트</string> + <string name="notification_boost_description">누군가 내 게시물을 부스트할 때 알림</string> + <string name="notification_favourite_name">좋아요</string> + <string name="notification_favourite_description">누군가 내 게시물을 좋아요 했을 때 알림</string> + <string name="notification_poll_name">투표</string> + <string name="notification_poll_description">투표가 종료되었을 때 알림</string> + <string name="notification_mention_format">%1$s님이 당신을 멘션했습니다</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s, 그 외 %4$d개</string> + <string name="notification_summary_medium">%1$s님, %2$s님, %3$s님</string> + <string name="notification_summary_small">%1$s와 %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="other">%1$d개의 새로운 알림이 있습니다</item> + </plurals> + <string name="description_account_locked">계정 잠김</string> + <string name="about_title_activity">이 앱에 관하여</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tusky는 무료이며 오픈 소스입니다. 이 프로젝트는 GNU General Public License Version 3에 의해 배포됩니다. 이 페이지에서 라이선스 전문(영문)을 열람하실 수 있습니다: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">프로젝트 홈페이지: +\n https://tusky.app</string> + <string name="about_bug_feature_request_site">버그 신고/건의사항: +\n https://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Tusky 공식 계정</string> + <string name="post_share_content">이 툿의 내용 공유</string> + <string name="post_share_link">이 툿의 링크 공유</string> + <string name="post_media_images">사진</string> + <string name="post_media_video">비디오</string> + <string name="state_follow_requested">팔로우 요청함</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">%1$d년 후</string> + <string name="abbreviated_in_days">%1$d일 후</string> + <string name="abbreviated_in_hours">%1$d시간 후</string> + <string name="abbreviated_in_minutes">%1$d분 후</string> + <string name="abbreviated_in_seconds">%1$d초 후</string> + <string name="abbreviated_years_ago">%1$d년</string> + <string name="abbreviated_days_ago">%1$d일</string> + <string name="abbreviated_hours_ago">%1$d시간</string> + <string name="abbreviated_minutes_ago">%1$d분</string> + <string name="abbreviated_seconds_ago">%1$d초</string> + <string name="follows_you">당신을 팔로우합니다</string> + <string name="pref_title_alway_show_sensitive_media">민감한 컨텐츠 항상 보이기</string> + <string name="title_media">미디어</string> + <string name="replying_to">\@%1$s에게 답장</string> + <string name="load_more_placeholder_text">더 불러오기</string> + <string name="pref_title_public_filter_keywords">공개 타임라인</string> + <string name="pref_title_thread_filter_keywords">대화</string> + <string name="filter_addition_title">필터 추가</string> + <string name="filter_edit_title">필터 편집</string> + <string name="filter_dialog_remove_button">삭제</string> + <string name="filter_dialog_update_button">변경 사항 저장</string> + <string name="filter_dialog_whole_word">단어 전체에 매칭</string> + <string name="filter_dialog_whole_word_description">키워드나 문장이 영숫자로만 이루어져 있을 경우, 단어 전체와 일치할 때에만 필터링합니다.</string> + <string name="filter_add_description">필터링할 문구 입력</string> + <string name="add_account_name">계정 추가</string> + <string name="add_account_description">마스토돈 계정을 추가합니다</string> + <string name="action_lists">리스트</string> + <string name="title_lists">리스트</string> + <string name="error_create_list">리스트를 만들 수 없습니다.</string> + <string name="error_rename_list">리스트의 이름을 변경할 수 없습니다.</string> + <string name="error_delete_list">리스트를 삭제할 수 없습니다.</string> + <string name="action_create_list">리스트 생성</string> + <string name="action_rename_list">리스트 이름 바꾸기</string> + <string name="action_delete_list">리스트 삭제</string> + <string name="hint_search_people_list">당신을 팔로우하는 사람 검색</string> + <string name="action_add_to_list">리스트에 계정 추가</string> + <string name="action_remove_from_list">리스트에서 계정 삭제</string> + <string name="compose_active_account_description">%1$s로서 포스팅</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">시각 장애인을 위한 설명 +\n(%1$d글자 작성 가능)</item> + </plurals> + <string name="action_set_caption">설명 추가</string> + <string name="action_remove">삭제</string> + <string name="lock_account_label">계정 잠금</string> + <string name="lock_account_label_description">팔로워를 수동으로 승인합니다</string> + <string name="compose_save_draft">작성 중인 내용을 저장하시겠습니까\?</string> + <string name="send_post_notification_title">툿을 보내고 있습니다…</string> + <string name="send_post_notification_error_title">툿을 보낼 수 없습니다</string> + <string name="send_post_notification_channel_name">툿을 보내고 있습니다</string> + <string name="send_post_notification_cancel_title">보내기 취소됨</string> + <string name="send_post_notification_saved_content">복사본이 임시 저장에 저장되었습니다</string> + <string name="action_compose_shortcut">글쓰기</string> + <string name="error_no_custom_emojis">이 인스턴스 %1$s 은(는) 커스텀 이모지가 없습니다.</string> + <string name="emoji_style">이모지 스타일</string> + <string name="system_default">시스템 기본</string> + <string name="download_fonts">시스템 기본 외의 이모지를 설정하시려면 우선 다운로드해야 합니다</string> + <string name="performing_lookup_title">탐색하고 있습니다…</string> + <string name="expand_collapse_all_posts">모두 보이기/줄이기</string> + <string name="action_open_post">툿 열기</string> + <string name="restart_required">어플리케이션 재시작 필요</string> + <string name="restart_emoji">변경 사항을 적용하려면 Tusky를 재시작해야 합니다</string> + <string name="later">다음에</string> + <string name="restart">지금 재시작</string> + <string name="caption_systememoji">이 디바이스의 기본 이모지</string> + <string name="caption_blobmoji">Android 4.4~7.1에 내장된 기본 이모지</string> + <string name="caption_twemoji">Mastodon 표준 이모지</string> + <string name="caption_notoemoji">현재 Google이 배포 중인 이모지</string> + <string name="download_failed">다운로드 실패</string> + <string name="profile_badge_bot_text">봇</string> + <string name="account_moved_description">%1$s는 계정을 이동했습니다:</string> + <string name="reblog_private">원래의 수신자들에게 부스트</string> + <string name="unreblog_private">부스트 해제</string> + <string name="license_description">Tusky에는 다음 오픈 소스 프로젝트의 요소/코드를 일부 활용하였습니다:</string> + <string name="license_apache_2">Apache License()에 의해 배포됨</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">프로필 메타데이터</string> + <string name="profile_metadata_add">항목 추가</string> + <string name="profile_metadata_label_label">라벨</string> + <string name="profile_metadata_content_label">내용</string> + <string name="pref_title_absolute_time">절대 시각 사용</string> + <string name="label_remote_account">다음 정보는 원본과 차이가 있을 수 있습니다. 브라우저로 프로필을 확인하시려면 여기를 누르세요.</string> + <string name="unpin_action">고정 해제</string> + <string name="pin_action">고정</string> + <plurals name="favs"> + <item quantity="other"><b>%1$s</b> 좋아요</item> + </plurals> + <plurals name="reblogs"> + <item quantity="other"><b>%1$s</b> 부스트</item> + </plurals> + <string name="title_reblogged_by">부스트한 유저</string> + <string name="title_favourited_by">좋아요한 유저</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s와 %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s 외 %3$d명</string> + <string name="description_post_media">미디어: %1$s</string> + <string name="description_post_cw">열람주의: %1$s</string> + <string name="description_post_media_no_description_placeholder">설명 없음</string> + <string name="description_post_reblogged">부스트함</string> + <string name="description_post_favourited">좋아요함</string> + <string name="description_visibility_public">공개</string> + <string name="description_visibility_unlisted">타임라인에 비표시</string> + <string name="description_visibility_private">비공개</string> + <string name="description_visibility_direct">다이렉트</string> + <string name="description_poll">투표 선택지: %1$s, %2$s, %3$s, %4$s, %5$s</string> + <string name="hint_list_name">리스트 이름</string> + <string name="edit_hashtag_hint">#를 제외한 해시태그</string> + <string name="notifications_clear">알림 지우기</string> + <string name="notifications_apply_filter">필터</string> + <string name="filter_apply">적용</string> + <string name="compose_shortcut_long_label">툿 작성하기</string> + <string name="compose_shortcut_short_label">작성</string> + <string name="notification_clear_text">모든 알림을 영구적으로 지우시겠습니까\?</string> + <string name="compose_preview_image_description">이미지 %1$s를...</string> + <string name="poll_info_format"> <!-- 15 명 참여 • 1 시간 남음 --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="other">%1$s 명 참여</item> + </plurals> + <string name="poll_info_time_absolute">%1$s에 종료</string> + <string name="poll_info_closed">마감됨</string> + <string name="poll_vote">투표</string> + <string name="poll_ended_voted">당신이 참여한 투표가 종료되었습니다</string> + <string name="poll_ended_created">당신이 시작한 투표가 종료되었습니다</string> + <!--These are for timestamps on polls --> + <plurals name="poll_timespan_days"> + <item quantity="other">%1$d일 남음</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="other">%1$d시간 남음</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="other">%1$d분 남음</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="other">%1$d초 남음</item> + </plurals> + <string name="button_continue">다음</string> + <string name="button_back">이전</string> + <string name="button_done">완료</string> + <string name="report_sent_success">\@%1$s를 신고하였습니다</string> + <string name="hint_additional_info">부가 설명</string> + <string name="report_remote_instance">%1$s에 포워딩</string> + <string name="failed_report">신고하지 못했습니다</string> + <string name="failed_fetch_posts">게시물을 불러오지 못했습니다</string> + <string name="report_description_1">인스턴스 관리자에게 신고합니다. 이 계정을 신고하려는 이유를 작성하실 수 있습니다:</string> + <string name="report_description_remote_instance">이 유저는 다른 인스턴스에 속해 있습니다. 그 인스턴스에도 익명으로 신고 내용을 보내시겠습니까\?</string> + <string name="action_open_as">%1$s(으)로 열기</string> + <string name="title_accounts">계정</string> + <string name="failed_search">검색하지 못했습니다</string> + <string name="pref_title_alway_open_spoiler">열람주의로 설정된 툿을 항상 펼치기</string> + <string name="action_add_poll">투표 추가</string> + <string name="create_poll_title">투표</string> + <string name="duration_5_min">5분</string> + <string name="duration_30_min">30분</string> + <string name="duration_1_hour">1시간</string> + <string name="duration_6_hours">6시간</string> + <string name="duration_1_day">1일</string> + <string name="duration_3_days">3일</string> + <string name="duration_7_days">7일</string> + <string name="add_poll_choice">항목 추가</string> + <string name="poll_allow_multiple_choices">여러 항목 선택 가능</string> + <string name="poll_new_choice_hint">%1$d번 항목</string> + <string name="edit_poll">수정</string> + <string name="action_edit">수정</string> + <string name="hashtags">해시태그</string> + <string name="notification_follow_request_name">팔로우 요청</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-large-land/dimens.xml b/app/src/main/res/values-large-land/dimens.xml new file mode 100644 index 0000000..8a9db25 --- /dev/null +++ b/app/src/main/res/values-large-land/dimens.xml @@ -0,0 +1,3 @@ +<resources> + <dimen name="compose_activity_scrollview_height">180dp</dimen> +</resources> diff --git a/app/src/main/res/values-large/dimens.xml b/app/src/main/res/values-large/dimens.xml new file mode 100644 index 0000000..046974e --- /dev/null +++ b/app/src/main/res/values-large/dimens.xml @@ -0,0 +1,6 @@ +<resources> + <dimen name="compose_activity_scrollview_height">400dp</dimen> + + <dimen name="status_media_preview_height">160dp</dimen> + <dimen name="status_detail_media_preview_height">180dp</dimen> +</resources> diff --git a/app/src/main/res/values-large/styles.xml b/app/src/main/res/values-large/styles.xml new file mode 100644 index 0000000..8d05f27 --- /dev/null +++ b/app/src/main/res/values-large/styles.xml @@ -0,0 +1,26 @@ +<resources> + <style name="TuskyDialogActivityTheme" parent="@style/TuskyTheme"> + <item name="android:windowFrame">@null</item> + <item name="android:windowBackground">@drawable/background_dialog_activity</item> + <item name="android:windowIsFloating">true</item> + <item name="android:windowContentOverlay">@null</item> + <item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item> + <item name="android:windowCloseOnTouchOutside">false</item> + <item name="android:windowActionModeOverlay">true</item> + <item name="android:windowMinWidthMajor">80%</item> + <item name="android:windowMinWidthMinor">80%</item> + </style> + + <style name="TuskyDialogActivityBlackTheme" parent="@style/TuskyBlackTheme"> + <item name="android:windowFrame">@null</item> + <item name="android:windowBackground">@drawable/background_dialog_activity</item> + <item name="android:windowIsFloating">true</item> + <item name="android:windowContentOverlay">@null</item> + <item name="android:windowSoftInputMode">stateUnspecified|adjustPan</item> + <item name="android:windowCloseOnTouchOutside">false</item> + <item name="android:windowActionModeOverlay">true</item> + <item name="android:windowMinWidthMajor">80%</item> + <item name="android:windowMinWidthMinor">80%</item> + </style> + +</resources> diff --git a/app/src/main/res/values-lb/strings.xml b/app/src/main/res/values-lb/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/main/res/values-lb/strings.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources></resources> \ No newline at end of file diff --git a/app/src/main/res/values-lv/strings.xml b/app/src/main/res/values-lv/strings.xml new file mode 100644 index 0000000..a0604a3 --- /dev/null +++ b/app/src/main/res/values-lv/strings.xml @@ -0,0 +1,602 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_invalid_domain">Ievadīts nederīgs domēns</string> + <string name="error_empty">Tas nevar būt tukšs.</string> + <string name="error_failed_app_registration">Autentificēšana ar šo instanci neizdevās. Ja tas atkārtojas, izmantojot izvēlni, mēģiniet pieslēgties pārlūkā.</string> + <string name="error_authorization_unknown">Radās neidentificēta autorizācijas kļūda. Ja tas atkārtojas, izmantojot izvēlni, mēģiniet pieslēgties pārlūkā.</string> + <string name="error_authorization_denied">Autorizācija tika liegta. Ja ir pārliecība, ka ievadīti pareizi pieslēgšanās dati, izmantojot izvēlni, mēģiniet pieslēgties pārlūkā.</string> + <string name="error_no_web_browser_found">Nevarēja atrast tīmekļa pārlūku, ko izmantot.</string> + <string name="error_could_not_load_login_page">Nevarēja ielādēt pieslēgšanās lapu.</string> + <string name="error_compose_character_limit">Ieraksts ir pārāk garš!</string> + <string name="error_loading_account_details">Neizdevās ielādēt konta datus</string> + <string name="error_image_edit_failed">Attēlu nevarēja rediģēt.</string> + <string name="error_media_upload_type">Šī veida failu nevar augšupielādēt.</string> + <string name="error_media_upload_opening">Šo failu nevarēja atvērt.</string> + <string name="error_media_upload_image_or_video">Ierakstam nevar vienlaicīgi pievienot gan attēlus, gan video.</string> + <string name="title_public_local">Vietējā</string> + <string name="title_notifications">Paziņojumi</string> + <string name="title_tab_preferences">Cilnes</string> + <string name="title_view_thread">Pavediens</string> + <string name="title_posts">Ieraksti</string> + <string name="title_bookmarks">Grāmatzīmes</string> + <string name="title_follows">Sekojamie</string> + <string name="title_mutes">Apklusinātie lietotāji</string> + <string name="title_drafts">Melnraksti</string> + <string name="title_edit_profile">Labot savu profilu</string> + <string name="title_domain_mutes">Paslēptie domēni</string> + <string name="title_follow_requests">Sekošanas pieprasījumi</string> + <string name="title_scheduled_posts">Ieplānotie ieraksti</string> + <string name="post_username_format">\@%1$s</string> + <string name="title_announcements">Paziņojumi</string> + <string name="post_sensitive_media_title">Sensitīvs saturs</string> + <string name="post_content_show_less">Sakļaut</string> + <string name="post_content_warning_show_more">Rādīt vairāk</string> + <string name="post_content_warning_show_less">Rādīt mazāk</string> + <string name="message_empty">Šeit nekā nav.</string> + <string name="notification_follow_format">%1$s sāka tev sekot</string> + <string name="notification_sign_up_format">%1$s reģistrējās</string> + <string name="footer_empty">Šeit nekā nav. Pavelciet uz leju, lai atjauninātu!</string> + <string name="notification_update_format">%1$s laboja savu ierakstu</string> + <string name="post_media_alt">ALT</string> + <string name="action_quick_reply">Ātrā atbilde</string> + <string name="action_reply">Atbildēt</string> + <string name="action_login">Pieslēgties ar Tusky</string> + <string name="action_bookmark">Atzīmēt ar grāmatzīmi</string> + <string name="action_unbookmark">Noņemt grāmatzīmi</string> + <string name="action_follow">Sekot</string> + <string name="action_unblock">Atbloķēt</string> + <string name="action_unfollow">Pārtraukt sekošanu</string> + <string name="action_edit">Labot</string> + <string name="action_delete">Dzēst</string> + <string name="action_close">Aizvērt</string> + <string name="action_view_profile">Profils</string> + <string name="action_delete_conversation">Dzēst sarunu</string> + <string name="action_view_preferences">Iestatījumi</string> + <string name="action_view_domain_mutes">Paslēptie domēni</string> + <string name="action_view_mutes">Apklusinātie lietotāji</string> + <string name="action_view_blocks">Bloķētie lietotāji</string> + <string name="action_view_media">Multivide</string> + <string name="action_share">Dalīties</string> + <string name="action_add_poll">Pievienot aptauju</string> + <string name="action_photo_take">Uzņemt fotoattēlu</string> + <string name="action_mute">Apklusināt</string> + <string name="action_mute_domain">Apklusināt %1$s</string> + <string name="action_edit_own_profile">Labot</string> + <string name="action_accept">Pieņemt</string> + <string name="action_reject">Noraidīt</string> + <string name="action_edit_profile">Labot profilu</string> + <string name="action_search">Meklēt</string> + <string name="action_hide_media">Paslēpt multividi</string> + <string name="action_undo">Atcelt</string> + <string name="action_content_warning">Satura brīdinājums</string> + <string name="action_toggle_visibility">Ieraksta redzamība</string> + <string name="action_schedule_post">Ieplānot ierakstu</string> + <string name="action_emoji_keyboard">Emocijzīmju tastatūra</string> + <string name="action_add_tab">Pievienot cilni</string> + <string name="action_reset_schedule">Atiestatīt</string> + <string name="action_mentions">Pieminējumi</string> + <string name="download_image">Lejupielādē %1$s</string> + <string name="title_mentions_dialog">Pieminējumi</string> + <string name="action_details">Detaļas</string> + <string name="title_hashtags_dialog">Tēmturi</string> + <string name="confirmation_reported">Nosūtīts!</string> + <string name="action_copy_link">Nokopēt saiti</string> + <string name="action_open_as">Atvērt kā %1$s</string> + <string name="confirmation_unblocked">Lietotājs atbloķēts</string> + <string name="hint_note">Biogrāfija</string> + <string name="hint_domain">Kura instance\?</string> + <string name="hint_content_warning">Satura brīdinājums</string> + <string name="label_avatar">Avatars</string> + <string name="login_connection">Savienojas…</string> + <string name="search_no_results">Nav rezultātu</string> + <string name="link_whats_an_instance">Kas ir instance\?</string> + <string name="label_quick_reply">Atbildēt…</string> + <string name="dialog_download_image">Lejupielādēt</string> + <string name="dialog_delete_post_warning">Dzēst šo ierakstu\?</string> + <string name="dialog_delete_conversation_warning">Dzēst šo sarunu\?</string> + <string name="pref_title_language">Valoda</string> + <string name="pref_title_post_tabs">Cilnes</string> + <string name="pref_title_post_filter">Laika līnijas filtrēšana</string> + <string name="pref_title_show_replies">Rādīt atbildes</string> + <string name="pref_title_animate_gif_avatars">Animēt GIF avatarus</string> + <string name="pref_title_proxy_settings">Starpniekserveris</string> + <string name="pref_title_http_proxy_settings">HTTP starpniekserveris</string> + <string name="pref_title_http_proxy_port">HTTP starpniekservera ports</string> + <string name="post_text_size_small">Mazs</string> + <string name="post_text_size_medium">Vidējs</string> + <string name="post_text_size_large">Liels</string> + <string name="post_text_size_largest">Lielākais</string> + <string name="pref_show_self_username_always">Vienmēr</string> + <string name="pref_show_self_username_never">Nekad</string> + <string name="notification_poll_name">Aptaujas</string> + <string name="notification_report_name">Ziņojumi</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="post_media_video">Video</string> + <string name="post_media_audio">Audio</string> + <string name="post_media_attachments">Pielikumi</string> + <string name="state_follow_requested">Sekošana pieprasīta</string> + <string name="pref_title_public_filter_keywords">Publiskās laika līnijas</string> + <string name="filter_addition_title">Pievienot filtru</string> + <string name="pref_title_thread_filter_keywords">Sarunas</string> + <string name="filter_dialog_remove_button">Noņemt</string> + <string name="filter_dialog_update_button">Atjaunināt</string> + <string name="action_lists">Saraksti</string> + <string name="title_lists">Saraksti</string> + <string name="filter_edit_title">Labot filtru</string> + <string name="add_account_name">Pievienot kontu</string> + <string name="error_delete_list">Nevarēja dzēst sarakstu</string> + <string name="action_create_list">Izveidot sarakstu</string> + <string name="action_rename_list">Pārsaukt sarakstu</string> + <string name="action_delete_list">Dzēst sarakstu</string> + <string name="action_add_to_list">Pievienot kontu sarakstam</string> + <string name="action_remove_from_list">Noņemt kontu no saraksta</string> + <string name="action_edit_image">Labot attēlu</string> + <string name="send_post_notification_title">Sūta ierakstu…</string> + <string name="send_post_notification_cancel_title">Sūtīšana atcelta</string> + <string name="compose_save_draft_loses_media">Vai saglabāt melnrakstu (atjaunojot melnrakstu, pielikumi tiks augšupielādēti vēlreiz)\?</string> + <string name="later">Vēlāk</string> + <string name="restart">Sākt no jauna</string> + <string name="system_default">Sistēmas noklusējuma</string> + <string name="action_open_post">Atvērt ierakstu</string> + <string name="profile_metadata_content_label">Saturs</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Profila metadati</string> + <string name="profile_metadata_add">pievienot datus</string> + <string name="conversation_more_recipients">%1$s, %2$s un %3$d citi</string> + <string name="description_post_media_no_description_placeholder">Nav apraksta</string> + <string name="description_post_language">Ieraksta valoda</string> + <string name="hint_list_name">Saraksta nosaukums</string> + <string name="select_list_title">Izvēlies sarakstu</string> + <string name="description_post_edited">Labots</string> + <string name="description_post_bookmarked">Atzīmēts ar grāmatzīmi</string> + <string name="description_visibility_public">Publisks</string> + <string name="hashtags">Tēmturi</string> + <string name="notifications_clear">Notīrīt</string> + <string name="notifications_apply_filter">Filtrēt</string> + <string name="filter_apply">Pielietot</string> + <string name="button_continue">Turpināt</string> + <string name="button_back">Atpakaļ</string> + <string name="button_done">Gatavs</string> + <string name="hint_additional_info">Papildu komentāri</string> + <string name="poll_info_closed">slēgta</string> + <string name="poll_vote">Balsot</string> + <string name="create_poll_title">Aptauja</string> + <string name="report_description_remote_instance">Konts ir no cita servera. Vai nosūtīt arī tam anonīmu ziņojuma kopiju\?</string> + <string name="duration_30_min">30 minūtes</string> + <string name="duration_indefinite">Bezgalīgi</string> + <string name="duration_1_hour">1 stundu</string> + <string name="duration_6_hours">6 stundas</string> + <string name="duration_1_day">1 dienu</string> + <string name="duration_3_days">3 dienas</string> + <string name="duration_7_days">7 dienas</string> + <string name="label_duration">Ilgums</string> + <string name="duration_30_days">30 dienas</string> + <string name="duration_60_days">60 dienas</string> + <string name="duration_180_days">180 dienas</string> + <string name="duration_365_days">365 dienas</string> + <string name="add_poll_choice">Pievienot izvēli</string> + <string name="poll_allow_multiple_choices">Vairākas izvēles</string> + <string name="poll_new_choice_hint">%1$d. izvēle</string> + <string name="duration_no_change">(Nav izmaiņu)</string> + <string name="pref_title_wellbeing_mode">Labsajūta</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="report_category_violation">Noteikumu pārkāpums</string> + <string name="report_category_other">Cits</string> + <string name="delete_scheduled_post_warning">Vai dzēst šo ieplānoto ierakstu\?</string> + <string name="error_generic">Notika kļūda.</string> + <string name="error_media_upload_sending">Augšupielāde neizdevās.</string> + <string name="error_rename_list">Nevarēja pārsaukt sarakstu</string> + <string name="action_add_or_remove_from_list">Pievienot vai noņemt no saraksta</string> + <string name="title_licenses">Licences</string> + <string name="about_title_activity">Par</string> + <string name="edit_poll">Labot</string> + <string name="action_access_drafts">Melnraksti</string> + <string name="action_block">Bloķēt</string> + <string name="action_report">Ziņot</string> + <string name="action_open_in_web">Atvērt pārlūkā</string> + <string name="duration_5_min">5 minūtes</string> + <string name="hint_search">Meklēt…</string> + <string name="title_accounts">Konti</string> + <string name="title_home">Sākums</string> + <string name="title_public_federated">Apvienotā</string> + <string name="title_followers">Sekotāji</string> + <string name="post_content_show_more">Izplest</string> + <string name="action_more">Vairāk</string> + <string name="action_retry">Mēģināt vēlreiz</string> + <string name="action_view_bookmarks">Grāmatzīmes</string> + <string name="action_save">Saglabāt</string> + <string name="action_links">Saites</string> + <string name="title_links_dialog">Saites</string> + <string name="dialog_message_uploading_media">Augšupielādē…</string> + <string name="pref_title_timeline_filters">Filtri</string> + <string name="pref_title_browser_settings">Pārlūks</string> + <string name="post_text_size_smallest">Mazākais</string> + <string name="post_media_images">Attēli</string> + <string name="action_remove">Noņemt</string> + <string name="profile_badge_bot_text">Bots</string> + <string name="description_visibility_private">Sekotāji</string> + <string name="list">Saraksts</string> + <string name="account_note_saved">Saglabāts!</string> + <string name="title_posts_with_replies">Ar atbildēm</string> + <string name="report_comment_hint">Papildu komentāri\?</string> + <string name="action_view_follow_requests">Sekošanas pieprasījumi</string> + <string name="action_add_reaction">pievienot reakciju</string> + <string name="hint_compose">Kas notiek\?</string> + <string name="dialog_block_warning">Bloķēt @%1$s\?</string> + <string name="pref_title_notification_filter_follow_requests">sekošana pieprasīta</string> + <string name="notification_follow_request_name">Sekošanas pieprasījumi</string> + <string name="notification_subscription_name">Jauni ieraksti</string> + <string name="follows_you">Seko tev</string> + <string name="load_more_placeholder_text">ielādēt vairāk</string> + <string name="compose_save_draft">Saglabāt melnrakstu\?</string> + <string name="download_failed">Lejupielāde neizdevās</string> + <string name="duration_14_days">14 dienas</string> + <string name="duration_90_days">90 dienas</string> + <string name="draft_deleted">Melnraksts dzēsts</string> + <string name="saving_draft">Saglabā melnrakstu…</string> + <string name="post_sensitive_media_directions">Klikšķini, lai apskatītu</string> + <string name="error_network">Radās tīkla kļūda. Lūdzu, pārbaudiet savienojumu un mēģiniet vēlreiz.</string> + <string name="about_tusky_license">Tusky ir bezmaksas un atvērtā pirmkoda programmatūra. Tā ir licencēta saskaņā ar GNU vispārējās publiskās licences 3. versiju. Licenci var apskatīt šeit: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="report_description_1">Ziņojums tiks nosūtīts jūsu servera moderatoram. Zemāk var sniegt skaidrojumu, kāpēc ziņojat par šo kontu:</string> + <string name="filter_dialog_whole_word_description">Ja atslēgvārds vai frāze ir tikai burtciparu, to pielietos tikai tad, ja tas atbildīs visam vārdam</string> + <string name="label_remote_account">Tālāk norādītā informācija var nepilnīgi atspoguļot lietotāja profilu. Nospiediet, lai pārlūkprogrammā atvērtu pilnu profilu.</string> + <string name="notification_report_format">Jauns ziņojums par %1$s</string> + <string name="action_mute_conversation">Apklusināt sarunu</string> + <string name="action_access_scheduled_posts">Ieplānotie ieraksti</string> + <string name="error_multimedia_size_limit">Video un audio faili nevar pārsniegt %1$s MB izmēru.</string> + <string name="error_following_hashtags_unsupported">Šī instance neatbalsta sekošanu tēmturiem.</string> + <string name="action_hashtags">Tēmturi</string> + <string name="send_post_notification_channel_name">Sūta ierakstus</string> + <string name="emoji_style">Emocijzīmju stils</string> + <string name="add_hashtag_title">Pievienot tēmturi</string> + <string name="instance_rule_title">%1$s noteikumi</string> + <string name="pref_title_show_cards_in_timelines">Rādīt saišu priekšskatījumus laika līnijās</string> + <string name="drafts_post_failed_to_send">Neizdevās nosūtīt šo ierakstu!</string> + <string name="title_blocks">Bloķētie lietotāji</string> + <string name="pref_title_http_proxy_enable">Iespējot HTTP starpniekserveri</string> + <string name="pref_title_http_proxy_server">HTTP starpniekserveris</string> + <string name="pref_title_http_proxy_port_message">Portam būtu jābūt starp %1$d un %2$d</string> + <string name="action_view_account_preferences">Konta iestatījumi</string> + <string name="mute_domain_warning">Vai tiešām vēlaties bloķēt visu %1$s\? Šī domēna saturs netiks rādīts ne publiskajās laika līnijās, ne paziņojumos. Jūsu sekotāji no šī domēna tiks noņemti.</string> + <string name="send_post_notification_saved_content">Ieraksta kopija tika saglabāta tavos melnrakstos</string> + <string name="restart_emoji">Lai pielietotu šīs izmaiņas, ir jāpārstartē Tusky</string> + <string name="error_status_source_load">Neizdevās ielādēt statusa avotu no servera.</string> + <string name="error_sender_account_gone">Sūtot ziņu, radās kļūda.</string> + <string name="title_login">Pieslēgties</string> + <string name="title_posts_pinned">Piesprausts</string> + <string name="title_followed_hashtags">Sekotie tēmturi</string> + <string name="post_boosted_format">%1$s pastiprināja</string> + <string name="post_edited">Labots %1$s</string> + <string name="notification_subscription_format">%1$s nupat publicēja</string> + <string name="report_username_format">Ziņot par @%1$s</string> + <string name="notification_header_report_format">%1$s ziņoja par %2$s</string> + <string name="action_logout">Iziet</string> + <string name="action_show_reblogs">Parādīt pastiprinājumus</string> + <string name="action_hide_reblogs">Paslēpt pastiprinājumus</string> + <string name="action_open_reblogger">Atvērt pastiprinājuma autoru</string> + <string name="action_open_reblogged_by">Parādīt pastiprinājumus</string> + <string name="action_share_as">Dalīties, izmantojot…</string> + <string name="send_post_content_to">Dalīties ar ierakstu…</string> + <string name="hint_display_name">Nosaukums</string> + <string name="dialog_mute_warning">Vai apklusināt @%1$s\?</string> + <string name="dialog_mute_hide_notifications">Paslēpt paziņojumus</string> + <string name="mute_domain_warning_dialog_ok">Paslēpt visu domēnu</string> + <string name="pref_title_edit_notification_settings">Paziņojumi</string> + <string name="pref_title_notifications_enabled">Paziņojumi</string> + <string name="pref_title_notification_alerts">Brīdinājumi</string> + <string name="pref_title_notification_filters">Paziņot man, kad</string> + <string name="pref_title_notification_filter_mentions">pieminēja</string> + <string name="pref_title_notification_filter_follows">sāka sekot</string> + <string name="pref_title_notification_filter_sign_ups">kāds ir reģistrējies</string> + <string name="app_theme_black">Melna</string> + <string name="app_theme_auto">Automātiski saulrietā</string> + <string name="app_theme_system">Izmantot sistēmas dizainu</string> + <string name="notification_mention_name">Jauni pieminējumi</string> + <string name="notification_follow_name">Jauni sekotāji</string> + <string name="notification_boost_name">Pastiprinājumi</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_years_ago">%1$dg</string> + <string name="filter_dialog_whole_word">Viss vārds</string> + <string name="unreblog_private">Atcelt pastiprināšanu</string> + <string name="unpin_action">Atspraust</string> + <string name="pin_action">Piespraust</string> + <string name="action_unfollow_hashtag_format">Vai pārtraukt sekot #%1$s\?</string> + <string name="about_tusky_account">Tusky profils</string> + <string name="performing_lookup_title">Meklē…</string> + <string name="mute_notifications_switch">Apklusināt paziņojumus</string> + <string name="action_reblog">Pastiprināt</string> + <string name="title_edits">Labojumi</string> + <string name="action_unreblog">Noņemt pastiprinājumu</string> + <string name="notification_update_name">Ierakstu labojumi</string> + <string name="send_media_to">Dalīties ar failu…</string> + <string name="description_account_locked">Privāts konts</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="lock_account_label">Padarīt kontu privātu</string> + <string name="review_notifications">Pārskatīt paziņojumus</string> + <string name="account_date_joined">Pievienojās %1$s</string> + <string name="action_discard">Atmest izmaiņas</string> + <string name="action_continue_edit">Turpināt labošanu</string> + <string name="pref_title_notification_alert_vibrate">Paziņot ar vibrāciju</string> + <string name="pref_title_notification_alert_light">Paziņot ar gaismu</string> + <string name="dialog_unfollow_warning">Vai pārtraukt sekot šim kontam\?</string> + <string name="pref_title_appearance_settings">Izskats</string> + <string name="pref_title_timelines">Laika līnijas</string> + <string name="app_them_dark">Tumša</string> + <string name="app_theme_light">Gaiša</string> + <string name="pref_title_app_theme">Lietotnes tēma</string> + <string name="pref_title_notification_filter_poll">aptaujas ir noslēgušās</string> + <string name="pref_main_nav_position_option_top">Augšā</string> + <string name="pref_main_nav_position_option_bottom">Apakšā</string> + <string name="pref_title_show_boosts">Parādīt pastiprinājumus</string> + <string name="status_count_one_plus">1+</string> + <string name="status_created_at_now">tikko</string> + <string name="error_retrieving_oauth_token">Neizdevās iegūt pieteikšanās pilnvaru. Ja tas atkārtojas, izmantojot izvēlni, mēģiniet pieslēgties pārlūkā.</string> + <string name="error_media_upload_permission">Nepieciešama atļauja lasīt multividi.</string> + <string name="error_media_download_permission">Nepieciešama atļauja saglabāt multividi.</string> + <string name="error_following_hashtag_format">Radās kļūda, sākot sekot #%1$s</string> + <string name="error_unfollowing_hashtag_format">Radās kļūda, pārtraucot sekot #%1$s</string> + <string name="error_muting_hashtag_format">Radās kļūda, apklusinot #%1$s</string> + <string name="error_unmuting_hashtag_format">Radās kļūda, atceļot #%1$s apklusināšanu</string> + <string name="title_direct_messages">Tiešās ziņas</string> + <string name="post_media_hidden_title">Multivide ir paslēpta</string> + <string name="notification_reblog_format">%1$s pastiprināja tavu ierakstu</string> + <string name="notification_follow_request_format">%1$s lūdza atļauju tev sekot</string> + <string name="notification_summary_report_format">%1$s · pievienoti %2$d ieraksti</string> + <string name="action_compose">Rakstīt</string> + <string name="action_logout_confirm">Vai tiešām vēlies iziet no konta %1$s\?</string> + <string name="action_add_media">Pievienot multividi</string> + <string name="action_open_drawer">Atvērt izvēlni</string> + <string name="action_open_media_n">Atvērt multividi #%1$d</string> + <string name="download_media">Lejupielādēt multividi</string> + <string name="downloading_media">Lejupielādē multividi</string> + <string name="hint_media_description_missing">Multividei ir jābūt aprakstam.</string> + <string name="dialog_title_finishing_media_upload">Pabeidz multivides augšupielādi</string> + <string name="pref_default_media_sensitivity">Vienmēr atzīmēt multividi kā sensitīvu</string> + <string name="error_no_custom_emojis">Instancē %1$s nav neviena pielāgota emocijzīmje</string> + <string name="download_fonts">Vispirms jālejupielādē šie emocijzīmju komplekti</string> + <string name="action_compose_shortcut">Rakstīt</string> + <string name="caption_blobmoji">Blob emocijzīmes no Android 4.4–7.1</string> + <string name="caption_systememoji">Tavas ierīces noklusējuma emocijzīmju komplekts</string> + <string name="license_description">Tusky satur kodu un sastāvdaļas no šādiem atvērtā pirmkoda projektiem:</string> + <string name="warning_scheduling_interval">Mastodon minimālais ieplānošanas intervāls ir 5 minūtes.</string> + <string name="no_scheduled_posts">Tev nav ieplānotu ziņu.</string> + <string name="account_note_hint">Tava privātā piezīme par šo kontu</string> + <string name="dialog_delete_list_warning">Vai tiešām vēlies dzēst sarakstu %1$s\?</string> + <string name="follow_requests_info">Lai arī tavs konts nav privāts, %1$s amatpersonas uzskatīja, ka, iespējams, vēlēsies manuāli pārskatīt sekošanas pieprasījumus no šiem kontiem.</string> + <string name="instance_rule_info">Pieslēdzoties tu piekrīti %1$s noteikumiem.</string> + <string name="compose_active_account_description">Publicē kā %1$s</string> + <string name="failed_to_add_to_list">Neizdevās pievienot kontu sarakstam</string> + <string name="failed_to_remove_from_list">Neizdevās noņemt kontu no saraksta</string> + <string name="license_apache_2">Licencēts saskaņā ar Apache licenci (kopija zemāk)</string> + <string name="compose_shortcut_long_label">Rakstīt ziņu</string> + <string name="compose_shortcut_short_label">Rakstīt</string> + <string name="poll_ended_voted">Aptauja, kurā tu balsoji, ir noslēgusies</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s un %4$d citi</string> + <string name="title_media">Multivide</string> + <string name="pref_title_alway_show_sensitive_media">Vienmēr rādīt sensitīvu saturu</string> + <string name="compose_unsaved_changes">Tev ir nesaglabātas izmaiņas.</string> + <string name="description_post_media">Multivide: %1$s</string> + <string name="description_poll">Aptauja ar izvēlēm: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="poll_info_format"> <!-- 15 balsis • atlikusi 1 stunda --> %1$s • %2$s</string> + <string name="notification_clear_text">Vai tiešām vēlies neatgriezeniski notīrīt visus paziņojumus\?</string> + <string name="poll_ended_created">Tevis izveidotā aptauja ir noslēgusies</string> + <string name="compose_preview_image_description">Darības ar attēlu %1$s</string> + <string name="tusky_compose_post_quicksetting_label">Rakstīt ziņu</string> + <string name="about_bug_feature_request_site">"Kļūdu ziņojumi un jaunas funkcionalitātes pieprasījumi: +\n https://github.com/tuskyapp/Tusky/issues"</string> + <string name="action_mention">Pieminēt</string> + <string name="pref_title_gradient_for_media">Rādīt krāsainus gradientus paslēptajai multividei</string> + <string name="pref_title_show_media_preview">Lejupielādēt multivides priekšskatījumus</string> + <string name="action_unmute">Atcelt apklusināšanu</string> + <string name="action_unmute_desc">Atcelt %1$s apklusināšanu</string> + <string name="send_post_link_to">Dalīties ar ieraksta URL…</string> + <string name="confirmation_unmuted">Lietotāja apklusināšana atcelta</string> + <string name="pref_title_notification_filter_reblogs">mani ieraksti tiek pastiprināti</string> + <string name="pref_title_custom_tabs">Izmantot Chrome pielāgotās cilnes</string> + <string name="action_unmute_domain">Atcelt %1$s apklusināšanu</string> + <string name="action_unmute_conversation">Atcelt sarunas apklusināšanu</string> + <string name="pref_main_nav_position">Galvenās navigācijas atrašanās vieta</string> + <string name="post_privacy_public">Publisks</string> + <string name="action_share_account_link">Dalīties ar saiti uz kontu</string> + <string name="action_share_account_username">Dalīties ar konta lietotājvārdu</string> + <string name="send_account_link_to">Dalīties ar konta URL…</string> + <string name="send_account_username_to">Dalīties ar konta lietotājvārdu…</string> + <string name="account_username_copied">Lietotājvārds nokopēts</string> + <string name="pref_title_notification_alert_sound">Paziņot ar skaņu</string> + <string name="label_header">Galvene</string> + <string name="pref_title_bot_overlay">Rādīt botu indikatoru</string> + <string name="pref_default_post_privacy">Noklusējuma ierakstu privātums</string> + <string name="pref_default_post_language">Noklusējuma ierakstu valoda</string> + <string name="pref_failed_to_sync">Neizdevās sinhronizēt iestatījumus</string> + <string name="pref_post_text_size">Ierakstu teksta izmērs</string> + <string name="notification_mention_descriptions">Paziņojumi par jauniem pieminējumiem</string> + <string name="notification_follow_request_description">Paziņojumi par sekošanas pieprasījumiem</string> + <string name="notification_boost_description">Paziņojumi par tavu ierakstu pastiprināšanu</string> + <string name="no_drafts">Tev nav neviena melnraksta.</string> + <string name="no_announcements">Nav paziņojumu.</string> + <string name="no_lists">Tev nav neviena saraksta.</string> + <string name="pref_title_show_self_username">Rādīt lietotājvārdu rīkjoslās</string> + <string name="pref_title_confirm_reblogs">Pirms pastiprināšanas rādīt apstiprināšanas dialogu</string> + <string name="failed_search">Meklēšana neizdevās</string> + <string name="pref_title_enable_swipe_for_tabs">Iespējot vilkšanas žestu, lai pārslēgtos starp cilnēm</string> + <string name="pref_title_hide_top_toolbar">Paslēpt augšējās rīkjoslas virsrakstu</string> + <string name="pref_title_notification_filter_reports">iesniegts jauns ziņojums</string> + <string name="report_remote_instance">Pārsūtīt uz %1$s</string> + <string name="failed_report">Ziņošana neizdevās</string> + <string name="notification_follow_description">Paziņojumi par jauniem sekotājiem</string> + <string name="notification_poll_description">Paziņojumi par pabeigtajām aptaujām</string> + <string name="notification_subscription_description">Paziņojumi par jauniem ierakstiem no kāda, kura ierakstus esi abonējis</string> + <string name="notification_sign_up_description">Paziņojumi par jauniem lietotājiem</string> + <string name="limit_notifications">Ierobežot laika līnijas paziņojumus</string> + <string name="wellbeing_hide_stats_profile">Slēpt profilu kvantitatīvo statistiku</string> + <string name="wellbeing_hide_stats_posts">Slēpt ierakstu kvantitatīvo statistiku</string> + <string name="drafts_failed_loading_reply">Neizdevās ielādēt atbildes informāciju</string> + <string name="action_delete_and_redraft">Dzēst un sākt no jauna</string> + <string name="action_dismiss">Aizvākt</string> + <string name="dialog_message_cancel_follow_request">Vai atcelt sekošanas pieprasījumu\?</string> + <string name="dialog_redraft_post_warning">Vai dzēst šo ierakstu un sākt no jauna\?</string> + <string name="about_powered_by_tusky">Darbību nodrošina Tusky</string> + <string name="hint_search_people_list">Meklēt personas, kurām seko</string> + <string name="send_post_notification_error_title">Sūtot ziņu, radās kļūda</string> + <string name="account_moved_description">%1$s ir pārcēlies uz:</string> + <string name="failed_to_pin">Piespraušana neizdevās</string> + <string name="failed_to_unpin">Atspraušana neizdevās</string> + <string name="post_share_content">Dalīties ar ieraksta saturu</string> + <string name="set_focus_description">Pieskaries vai velc apli, lai izvēlētos fokusa punktu, kas vienmēr būs redzams sīktēlos.</string> + <string name="expand_collapse_all_posts">Izplest/sakļaut visus ierakstus</string> + <string name="caption_twemoji">Mastodon standarta emocijzīmju komplekts</string> + <string name="caption_notoemoji">Google aktuālais emocijzīmju komplekts</string> + <string name="notification_report_description">Paziņojumi par moderēšanas ziņojumiem</string> + <string name="notification_mention_format">%1$s pieminēja tevi</string> + <string name="notification_summary_small">%1$s un %2$s</string> + <string name="notification_summary_medium">%1$s, %2$s un %3$s</string> + <string name="post_share_link">Dalīties ar saiti uz ierakstu</string> + <string name="pref_title_absolute_time">Izmantot absolūto laiku</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s un %2$s</string> + <string name="description_post_cw">Satura brīdinājums: %1$s</string> + <string name="about_project_site">Projekta vietne: +\n https://tusky.app</string> + <string name="a11y_label_loading_thread">Ielādē pavedienu</string> + <string name="confirmation_hashtag_unfollowed">pārtraukta sekošana #%1$s</string> + <string name="confirmation_domain_unmuted">%1$s atcelta slēpšana</string> + <string name="error_create_list">Nevarēja izveidot sarakstu</string> + <string name="pref_title_reading_order">Lasīšanas secība</string> + <string name="filter_add_description">Filtrējamā frāze</string> + <string name="pref_summary_http_proxy_disabled">Atspējots</string> + <string name="pref_summary_http_proxy_missing"><nav iestatīts></string> + <string name="pref_summary_http_proxy_invalid"><nederīgs></string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="add_account_description">Pievienot jaunu Mastodon kontu</string> + <string name="pref_title_animate_custom_emojis">Animēt pielāgotās emocijzīmes</string> + <string name="notification_sign_up_name">Jaunu dalībnieku reģistrācija</string> + <string name="pref_show_self_username_disambiguate">Kad pievienoti vairāki konti</string> + <string name="restart_required">Nepieciešama lietotnes restartēšana</string> + <string name="reblog_private">Pastiprināt sākotnējai auditorijai</string> + <string name="description_post_reblogged">Pastiprināts</string> + <string name="replying_to">Atbildot @%1$s</string> + <string name="lock_account_label_description">Prasa manuāli apstiprināt sekotājus</string> + <string name="title_reblogged_by">Pastiprināja</string> + <plurals name="reblogs"> + <item quantity="zero"><b>%1$s</b> pastiprinājumi</item> + <item quantity="one"><b>%1$s</b> pastiprinājums</item> + <item quantity="other"><b>%1$s</b> pastiprinājumi</item> + </plurals> + <string name="title_favourites">Izlases</string> + <string name="notification_favourite_format">%1$s pievienoja tavu ierakstu izlasei</string> + <string name="action_favourite">Pievienot izlasei</string> + <string name="action_unfavourite">Noņemt no izlases</string> + <string name="action_send">PUBLICĒT</string> + <string name="action_send_public">PUBLICĒT!</string> + <string name="action_open_faved_by">Rādīt izlases</string> + <string name="pref_publishing">Publicē (sinhronizēts ar serveri)</string> + <string name="notification_favourite_name">Izlases</string> + <string name="report_category_spam">Mēstule</string> + <string name="action_set_focus">Iestatīt fokusa punktu</string> + <plurals name="poll_info_votes"> + <item quantity="zero">%1$s balsis</item> + <item quantity="one">%1$s balss</item> + <item quantity="other">%1$s balsis</item> + </plurals> + <string name="pref_reading_order_oldest_first">Vecākos vispirms</string> + <string name="pref_reading_order_newest_first">Jaunākos vispirms</string> + <string name="status_created_info">%1$s izveidoja</string> + <plurals name="poll_info_people"> + <item quantity="zero">%1$s personas</item> + <item quantity="one">%1$s persona</item> + <item quantity="other">%1$s personas</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="zero">atlikušas %1$d stundas</item> + <item quantity="one">atlikusi %1$d stunda</item> + <item quantity="other">atlikušas %1$d stundas</item> + </plurals> + <plurals name="poll_timespan_days"> + <item quantity="zero">atlikušas %1$d dienas</item> + <item quantity="one">atlikusi %1$d diena</item> + <item quantity="other">atlikušas %1$d dienas</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="zero">atlikušas %1$d minūtes</item> + <item quantity="one">atlikusi %1$d minūte</item> + <item quantity="other">atlikušas %1$d minūtes</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="zero">atlikušas %1$d sekundes</item> + <item quantity="one">atlikusi %1$d sekunde</item> + <item quantity="other">atlikušas %1$d sekundes</item> + </plurals> + <string name="action_post_failed">Augšupielāde neizdevās</string> + <string name="action_post_failed_show_drafts">Rādīt melnrakstus</string> + <string name="action_post_failed_do_nothing">Aizvākt</string> + <string name="edit_hashtag_hint">Tēmturis bez #</string> + <string name="status_edit_info">%1$s laboja</string> + <string name="action_browser_login">Pieslēgties ar pārlūku</string> + <string name="description_login">Strādā vairumā gadījumu. Dati netiek nopludināti uz citām lietotnēm.</string> + <string name="poll_info_time_absolute">beidzas %1$s</string> + <string name="pref_title_confirm_favourites">Pirms pievienošanas izlasei rādīt apstiprināšanas dialogu</string> + <string name="action_view_favourites">Izlases</string> + <string name="pref_title_notification_filter_favourites">mani ieraksti tiek pievienoti izlasei</string> + <string name="abbreviated_in_days">pēc %1$dd</string> + <string name="abbreviated_in_hours">pēc %1$dh</string> + <string name="abbreviated_in_minutes">pēc %1$dm</string> + <string name="abbreviated_in_years">pēc %1$dg</string> + <string name="abbreviated_in_seconds">pēc %1$ds</string> + <string name="pref_title_alway_open_spoiler">Vienmēr izvērst ziņas, kas atzīmētas ar satura brīdinājumu</string> + <string name="action_post_failed_detail">Neizdevās augšupielādēt tavu ierakstu un tas tika saglabāts melnrakstos. +\n +\nVai nu neizdevās sazināties ar serveri, vai arī tas noraidīja ierakstu.</string> + <string name="action_post_failed_detail_plural">Neizdevās augšupielādēt tavus ierakstus un tie tika saglabāti melnrakstos. +\n +\nVai nu neizdevās sazināties ar serveri, vai arī tas noraidīja ierakstus.</string> + <string name="notification_favourite_description">Paziņojumi par tavu ierakstu pievienošanu izlasei</string> + <string name="title_migration_relogin">Pieslēdzies vēlreiz, lai iespējotu pašpiegādātos paziņojumus</string> + <string name="wellbeing_mode_notice">Daļa informācijas, kas varētu ietekmēt tavu garīgo labsajūtu, tiks slēpta. Tas ietver: +\n +\n - Pievienošanas izlasei/pastiprināšanas/jaunu sekotāju paziņojumus +\n - Izlasēm pievienoto/pastiprinājumu skaitu ierakstiem +\n - Sekotāju/ierakstu skaitu profilos +\n +\n Pašpiegādātie paziņojumi netiks ietekmēti, bet paziņojumu preferences var pārskatīt manuāli.</string> + <string name="description_browser_login">Var atbalstīt papildu autentifikācijas metodes, bet ir nepieciešama atbalstīta pārlūkprogramma.</string> + <string name="dialog_whats_an_instance">Šeit var ievadīt jebkuras instances adresi vai domēnu, piemēram, mastodon.social, icosahedron.website, social.tchncs.de un <a href="https://instances.social">citus!</a> +\n +\nJa tev vēl nav konta, vari ievadīt tās instances nosaukumu, kurai vēlies pievienoties un izveidot kontu. +\n +\nInstance ir vieta, kur tiek mitināts tavs konts, taču tu vari viegli sazināties ar cilvēkiem citās instancēs un sekot tiem, it kā atrastos tajā pašā vietnē. +\n +\nVairāk informācijas var atrast <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="failed_fetch_posts">Neizdevās iegūt ierakstus</string> + <string name="accessibility_talking_about_tag">%1$d cilvēki runā par tēmturi %2$s</string> + <string name="total_usage">Kopējais lietojums</string> + <string name="total_accounts">Konti kopā</string> + <string name="dialog_follow_hashtag_title">Sekot tēmturim</string> + <string name="dialog_follow_hashtag_hint">#tēmturis</string> + <string name="action_refresh">Atsvaizdināt</string> + <string name="ui_error_unknown">nezināms iemesls</string> + <string name="title_public_trending_hashtags">Tendenču tēmturi</string> + <string name="action_add">Pievienot</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="label_filter_context">Filtra konteksts</string> + <string name="label_filter_action">Filtra darbība</string> + <string name="hint_filter_title">Mans filtrs</string> + <string name="label_filter_title">Nosaukums</string> + <string name="ui_success_accepted_follow_request">Sekošanas pieprasījums pieņemts</string> + <string name="ui_success_rejected_follow_request">Sekošanas pieprasījums bloķēts</string> + <string name="post_privacy_followers_only">Tikai sekotājiem</string> + <string name="post_media_image">Attēls</string> + <string name="filter_description_hide">Paslēpt pilnībā</string> + <string name="select_list_manage">Pārvaldīt sarakstus</string> + <string name="filter_keyword_display_format">%1$s (viss vārds)</string> + <string name="pref_title_account_filter_keywords">Profili</string> + <string name="filter_action_warn">Brīdināt</string> + <string name="filter_action_hide">Paslēpt</string> + <string name="filter_description_warn">Paslēpt ar brīdinājumu</string> + <string name="filter_edit_keyword_title">Labot atslēgvārdu</string> + <string name="filter_keyword_addition_title">Pievienot atslēgvārdu</string> + <string name="dialog_delete_filter_positive_action">Dzēst</string> + <string name="list_reply_policy_label">Rāda atbildes uz</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-ml/strings.xml b/app/src/main/res/values-ml/strings.xml new file mode 100644 index 0000000..eb6d411 --- /dev/null +++ b/app/src/main/res/values-ml/strings.xml @@ -0,0 +1,162 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="action_login">മസ്റ്റഡോൺ വഴി പ്രവേശിക്കുക</string> + <string name="link_whats_an_instance">എന്താണ് ഒരു ഇൻസ്റ്റൻസ്\?</string> + <string name="title_favourites">പ്രിയപ്പെട്ടവ</string> + <string name="title_drafts">കരടുകൾ</string> + <string name="action_logout">പുറത്തിറങ്ങുക</string> + <string name="action_view_preferences">മുൻഗണനകൾ</string> + <string name="action_view_account_preferences">അക്കൗണ്ട് മുൻഗണനകൾ</string> + <string name="action_edit_profile">പ്രൊഫൈൽ തിരുത്തുക</string> + <string name="action_search">തിരയുക</string> + <string name="about_title_activity">വിവരം</string> + <string name="action_lists">പട്ടികകൾ</string> + <string name="title_lists">പട്ടികകൾ</string> + <string name="error_generic">ഒരു പിഴവ് സംഭവിച്ചിരിക്കുന്നു.</string> + <string name="error_network">ഒരു നെറ്റ്വർക്ക് പിഴവ് സംഭവിച്ചിരിക്കുന്നു! ദയവായി താങ്കളുടെ കണക്ഷൻ പരിശോധിച്ചിട്ട് വീണ്ടും ശ്രമിക്കൂ!</string> + <string name="error_empty">ഇത് ശൂന്യമാവാൻ പാടില്ല.</string> + <string name="error_invalid_domain">അസാധുവായ ഡൊമൈൻ നൽകിയിരിക്കുന്നു</string> + <string name="error_failed_app_registration">ആ ഇൻസ്റ്റൻസുമായി ആധികാരികത ഉറപ്പുവരുത്തുന്നതിൽ പരാജയപ്പെട്ടിരിക്കുന്നു.</string> + <string name="error_no_web_browser_found">ഉപയോഗിക്കാനായി ഒരു വെബ് ബ്രൗസർ കണ്ടെത്താനായില്ല.</string> + <string name="error_authorization_unknown">അജ്ഞാതമായ ഒരു ആധികാരികതാപിഴവ് സംഭവിച്ചിരിക്കുന്നു.</string> + <string name="error_authorization_denied">ആധികാരികത ഉറപ്പുവരുത്താനായില്ല.</string> + <string name="error_retrieving_oauth_token">ഒരു പ്രവേശന ടോക്കൺ ലഭ്യമാക്കുന്നതിൽ പരാജയപ്പെട്ടു.</string> + <string name="error_compose_character_limit">ഈ സ്റ്റാറ്റസ് വളരെ നീളമേറിയതാണ്!</string> + <string name="error_media_upload_type">ഇത്തരം ഫയൽ അപ്ലോഡ് ചെയ്യാൻ സാധിക്കില്ല.</string> + <string name="error_media_upload_opening">ഈ ഫയൽ തുറക്കാനായില്ല.</string> + <string name="error_media_upload_permission">മീഡിയ വായിക്കുവാനുള്ള അനുമതി ആവശ്യമാണ്.</string> + <string name="error_media_download_permission">മീഡിയ സംഭരിക്കുവാനുള്ള അനുമതി ആവശ്യമാണ്.</string> + <string name="error_media_upload_image_or_video">ചിത്രങ്ങളും ചലച്ചിത്രങ്ങളും ഒരുമിച്ച് ഒരു സ്റ്റാറ്റസിലേക്ക് ചേർക്കാനാവില്ല.</string> + <string name="error_media_upload_sending">അപ്ലോഡ് പരാജയപ്പെട്ടു.</string> + <string name="error_sender_account_gone">ടൂട്ട് അയയ്ക്കുന്നതിൽ പിഴവ്.</string> + <string name="title_home">പൂമുഖം</string> + <string name="title_notifications">അറിയിപ്പുകൾ</string> + <string name="title_public_local">ലോക്കൽ</string> + <string name="title_public_federated">ഫെഡറേറ്റഡ്</string> + <string name="title_direct_messages">നേരേയുള്ള സന്ദേശങ്ങൾ</string> + <string name="title_tab_preferences">ടാബുകൾ</string> + <string name="title_view_thread">ടൂട്ട്</string> + <string name="title_posts">പോസ്റ്റുകൾ</string> + <string name="title_posts_with_replies">മറുപടികളോടൊപ്പം</string> + <string name="title_posts_pinned">പിൻ ചെയ്തത്</string> + <string name="title_follows">പിന്തുടരലുകൾ</string> + <string name="title_followers">പിന്തുടരുന്നവർ</string> + <string name="title_mutes">നിശ്ശബ്ദരാക്കിയ ഉപയോക്താക്കൾ</string> + <string name="title_blocks">നിരോധിച്ച ഉപയോഗതാക്കൾ</string> + <string name="title_follow_requests">പിന്തുടാനുള്ള അപേക്ഷകൾ</string> + <string name="title_edit_profile">പ്രൊഫൈൽ തിരുത്തുക</string> + <string name="action_reply">മറുപടി</string> + <string name="action_reblog">ബൂസ്റ്റ്</string> + <string name="action_unreblog">ബൂസ്റ്റ് പിൻവലിക്കുക</string> + <string name="action_favourite">ഇഷ്ടപ്പെട്ടവ</string> + <string name="action_unfavourite">ഇഷ്ടപ്പെട്ടവയിൽ നിന്നും കളയുക</string> + <string name="action_more">കൂടുതൽ</string> + <string name="action_follow">പിന്തുടരുക</string> + <string name="action_unfollow">പിന്തുടരുന്നത് അവസാനിപ്പിക്കുക</string> + <string name="action_retry">വീണ്ടും ശ്രമിക്കുക</string> + <string name="action_view_follow_requests">പിന്തുടരുവാനുള്ള അഭ്യര്ത്ഥനകള്</string> + <string name="title_domain_mutes">മറച്ചുവെച്ച ഡൊമൈനുകൾ</string> + <string name="description_visibility_private">പിന്തുടരുന്നവർ</string> + <string name="footer_empty">പുതിയത് ലഭിക്കാൻ താഴേക്ക് വലിക്കുക</string> + <string name="message_empty">ലഭ്യമല്ല</string> + <string name="post_content_show_less">ചുരുക്കുക</string> + <string name="post_content_show_more">വിപുലപ്പെടുത്തുക</string> + <string name="post_content_warning_show_less">അൽപ്പം കാണിക്കൂ</string> + <string name="post_content_warning_show_more">കൂടുതൽ കാണിക്കൂ</string> + <string name="post_sensitive_media_directions">തുറന്ന് കാണുവാൻ</string> + <string name="post_media_hidden_title">മറയ്ക്കപ്പെട്ട മീഡിയ</string> + <string name="post_sensitive_media_title">സെൻസിറ്റീവ് ഉള്ളടക്കം</string> + <string name="title_licenses">അനുവാദം</string> + <string name="title_scheduled_posts">മുന്നിശ്ചയിച്ച ടൂറ്റ്സ്</string> + <string name="title_bookmarks">ബുക് മാർക്ക്</string> + <string name="action_reset_schedule">പുനഃക്രമീകരിക്കുക</string> + <string name="action_open_in_web">ബ്രൗസറിൽ തുറക്കുക</string> + <string name="action_view_media">മാധ്യമം</string> + <string name="action_view_domain_mutes">മറയ്ക്കപ്പെട്ട ഡൊമൈനുകൾ</string> + <string name="action_view_blocks">നിരോധിച്ച ഉപയോഗതാക്കൾ</string> + <string name="action_view_mutes">നിശ്ശബ്ദമാക്കിയ ഉപയോഗതാക്കൾ</string> + <string name="action_view_bookmarks">ബൂക്കമാർക്ക്</string> + <string name="action_view_favourites">ഇഷ്ടപ്പെട്ടത്</string> + <string name="action_view_profile">പ്രൊഫൈൽ</string> + <string name="action_close">നിർത്തുക</string> + <string name="action_send_public">ടൂട്ട്!</string> + <string name="action_send">ടൂട്ട്</string> + <string name="action_delete_and_redraft">നീക്കം ചെയ്യത് പുതിയത് എഴുതുക</string> + <string name="action_delete">നീക്കം ചെയ്യുക</string> + <string name="action_edit">തിരുത്ത്</string> + <string name="action_report">അറിയിക്കുക</string> + <string name="action_show_reblogs">ബൂട്ട്സ് കാണിക്കുക</string> + <string name="action_hide_reblogs">ബൂട്ട്സ് മറയ്ക്കുക</string> + <string name="action_unblock">നിരോധനം നീക്കുക</string> + <string name="action_block">നിരോധിക്കുക</string> + <string name="action_logout_confirm">ഈ അക്കൗണ്ടിൽ നിന്നും പുറത്തു പോകാൻ ഉറപ്പിച്ചോ %1$s\?</string> + <string name="action_compose">എഴുതുക</string> + <string name="action_quick_reply">പെട്ടന്നുള്ള മറുപടി</string> + <string name="report_comment_hint">കൂടുതൽ പറയാൻ ഉണ്ടോ\?</string> + <string name="report_username_format">അറിയിക്കുക @%1$s</string> + <string name="notification_follow_format">%1$s നിങ്ങളെ പിന്തുടരുന്നു</string> + <string name="notification_favourite_name">പ്രിയപ്പെട്ടവ</string> + <string name="title_media">മാധ്യമം</string> + <string name="action_open_reblogged_by">ബൂട്ട്സ് കാണിക്കുക</string> + <string name="edit_poll">തിരുത്ത്</string> + <string name="compose_shortcut_short_label">എഴുതുക</string> + <string name="action_compose_shortcut">എഴുതുക</string> + <string name="notification_follow_request_name">പിന്തുടാനുള്ള അപേക്ഷകൾ</string> + <string name="pref_title_show_boosts">ബൂട്ട്സ് കാണിക്കുക</string> + <string name="action_access_scheduled_posts">മുന്നിശ്ചയിച്ച ടൂറ്റ്സ്</string> + <string name="action_access_drafts">കരടുകൾ</string> + <string name="action_edit_own_profile">തിരുത്ത്</string> + <string name="pref_title_edit_notification_settings">അറിയിപ്പുകൾ</string> + <string name="pref_title_post_tabs">ടാബുകൾ</string> + <string name="pref_title_notifications_enabled">അറിയിപ്പുകൾ</string> + <string name="title_announcements">പ്രഖ്യാപനങ്ങൾ</string> + <string name="later">പിന്നീട്</string> + <string name="account_note_saved">സംരക്ഷിച്ചു!</string> + <string name="post_boosted_format">%1$s ബൂസ്റ്റ് ചെയ്തു</string> + <string name="search_no_results">ഫലങ്ങൾ ഒന്നും ഇല്ല</string> + <string name="confirmation_reported">അയച്ചൂ!</string> + <string name="action_share">പങ്കിടുക</string> + <string name="pref_title_browser_settings">ബ്രൗസർ</string> + <string name="notification_mention_name">പുതിയ സൂചനകൾ</string> + <string name="action_links">ലിങ്കുകൾ</string> + <string name="unreblog_private">ബൂസ്റ്റ് ചെയ്യേണ്ട</string> + <string name="filter_apply">പ്രയോഗിക്കുക</string> + <string name="add_account_name">അക്കൗണ്ട് ചേർക്കുക</string> + <string name="pref_title_notification_filter_mentions">സൂചിപ്പിച്ചു</string> + <string name="profile_badge_bot_text">യന്ത്രം</string> + <string name="pref_title_appearance_settings">രൂപം</string> + <string name="follows_you">നിങ്ങളെ പിന്തുടരുന്നു</string> + <string name="action_photo_take">ഫോട്ടോ എടുക്കുക</string> + <string name="hint_search">തിരയുക…</string> + <string name="post_media_images">ചിത്രങ്ങൾ</string> + <string name="load_more_placeholder_text">കൂടുതൽ ലഭ്യമാക്കുക</string> + <string name="title_mentions_dialog">സൂചനകൾ</string> + <string name="hint_note">ബയോ</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="pref_title_thread_filter_keywords">സംഭാഷണങ്ങൾ</string> + <string name="label_quick_reply">മറുപടി…</string> + <string name="action_reject">നിരസിക്കുക</string> + <string name="app_theme_black">കറുപ്പ്</string> + <string name="button_continue">തുടരുക</string> + <string name="pref_title_timelines">സമയരേഖകൾ</string> + <string name="pref_title_proxy_settings">പ്രോക്സി</string> + <string name="pref_title_notification_alerts">മുന്നറിയിപ്പുകൾ</string> + <string name="notifications_clear">മായ്ക്കുക</string> + <string name="action_remove">നീക്കം ചെയ്യുക</string> + <string name="notification_boost_name">ബൂസ്റ്റുകൾ</string> + <string name="action_add_media">മീഡിയ ചേർക്കുക</string> + <string name="title_accounts">അക്കൗണ്ടുകൾ</string> + <string name="pref_title_show_replies">മറുപടികൾ കാണിക്കൂ</string> + <string name="action_save">സംരക്ഷിക്കുക</string> + <string name="pref_title_timeline_filters">ഫിൽടറുകൾ</string> + <string name="pref_title_language">ഭാഷ</string> + <string name="profile_metadata_content_label">ഉള്ളടക്കം</string> + <string name="action_mentions">സൂചനകൾ</string> + <string name="filter_dialog_update_button">പുതുക്കുക</string> + <string name="label_avatar">അവതാർ</string> + <string name="title_links_dialog">ലിങ്കുകൾ</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_media_video">വിഡിയോ</string> + <string name="action_mention">സൂചിപ്പിക്കുക</string> + <string name="filter_dialog_remove_button">നീക്കം ചെയ്യുക</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 0000000..ece7341 --- /dev/null +++ b/app/src/main/res/values-nb-rNO/strings.xml @@ -0,0 +1,659 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">En feil har oppstått.</string> + <string name="error_network">En nettverksfeil har oppstått. Sjekk tilkoblingen, og prøv igjen.</string> + <string name="error_empty">Denne kan ikke være tom.</string> + <string name="error_invalid_domain">Ugyldig domene</string> + <string name="error_failed_app_registration">Kunne ikke autentisere med den instansen. Hvis dette fortsetter å skje, prøv å logge inn i nettleseren fra menyen.</string> + <string name="error_no_web_browser_found">Fant ingen nettleser som kunne brukes.</string> + <string name="error_authorization_unknown">En ukjent autoriseringsfeil oppsto. Hvis dette fortsetter, prøv å logge inn i nettleseren fra menyen.</string> + <string name="error_authorization_denied">Autorisasjon ble nektet. Hvis du er sikker på at du ga riktige data, prøv å logge inn i nettleseren fra menyen.</string> + <string name="error_retrieving_oauth_token">Henting av logintoken feilet. Hvis dette fortsetter prøv å logge in i nettleseren fra menyen.</string> + <string name="error_compose_character_limit">Innlegget er for langt!</string> + <string name="error_media_upload_type">Den filtypen kan ikke lastes opp.</string> + <string name="error_media_upload_opening">Den filen kunne ikke åpnes.</string> + <string name="error_media_upload_permission">Trenger tillatelse til å lese media.</string> + <string name="error_media_download_permission">Trenger tillatelse for å lagre media.</string> + <string name="error_media_upload_image_or_video">Bilder og videoer kan ikke legges til samme innlegg.</string> + <string name="error_media_upload_sending">Opplastingen feilet.</string> + <string name="error_sender_account_gone">En feil oppsto under sending av innlegget.</string> + <string name="title_home">Hjem</string> + <string name="title_notifications">Varsler</string> + <string name="title_public_local">Lokal</string> + <string name="title_public_federated">Føderert</string> + <string name="title_direct_messages">Direktemeldinger</string> + <string name="title_tab_preferences">Faner</string> + <string name="title_view_thread">Tråd</string> + <string name="title_posts">Innlegg</string> + <string name="title_posts_with_replies">Med svar</string> + <string name="title_posts_pinned">Festet</string> + <string name="title_follows">Følger</string> + <string name="title_followers">Følgere</string> + <string name="title_favourites">Favoritter</string> + <string name="title_mutes">Dempede brukere</string> + <string name="title_blocks">Blokkerte brukere</string> + <string name="title_follow_requests">Følgeforespørsler</string> + <string name="title_edit_profile">Endre profilen din</string> + <string name="title_drafts">Utkast</string> + <string name="title_licenses">Lisenser</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s delte</string> + <string name="post_sensitive_media_title">Sensitivt innhold</string> + <string name="post_media_hidden_title">Media skjult</string> + <string name="post_sensitive_media_directions">Trykk for å vise</string> + <string name="post_content_warning_show_more">Vis mer</string> + <string name="post_content_warning_show_less">Vis mindre</string> + <string name="post_content_show_more">Utvid</string> + <string name="post_content_show_less">Skjul</string> + <string name="message_empty">Ingenting her.</string> + <string name="footer_empty">Ingenting her. Dra ned for å oppdatere!</string> + <string name="notification_reblog_format">%1$s delte innlegget ditt</string> + <string name="notification_favourite_format">%1$s favoriserte innlegget ditt</string> + <string name="notification_follow_format">%1$s følger deg</string> + <string name="report_username_format">Rapporter @%1$s</string> + <string name="report_comment_hint">Ytterligere kommentarer\?</string> + <string name="action_quick_reply">Hurtigsvar</string> + <string name="action_reply">Svar</string> + <string name="action_reblog">Del</string> + <string name="action_unreblog">Fjern deling</string> + <string name="action_favourite">Legg til i favoritter</string> + <string name="action_unfavourite">Fjern favoritt</string> + <string name="action_more">Mer</string> + <string name="action_compose">Skriv</string> + <string name="action_login">Logg inn med Tusky</string> + <string name="action_logout">Logg ut</string> + <string name="action_logout_confirm">Er du sikker på at du vil logge ut fra kontoen %1$s\? Dette vil slette all lokal data for kontoen, som utkast og instillinger.</string> + <string name="action_follow">Følg</string> + <string name="action_unfollow">Slutt å følge</string> + <string name="action_block">Blokkér</string> + <string name="action_unblock">Fjern blokkering</string> + <string name="action_hide_reblogs">Skjul delinger</string> + <string name="action_show_reblogs">Vis delte inlegg</string> + <string name="action_report">Rapporter</string> + <string name="action_delete">Slett</string> + <string name="action_send">TUT</string> + <string name="action_send_public">TUT!</string> + <string name="action_retry">Prøv igjen</string> + <string name="action_close">Steng</string> + <string name="action_view_profile">Profil</string> + <string name="action_view_preferences">Innstillinger</string> + <string name="action_view_account_preferences">Kontoinnstillinger</string> + <string name="action_view_favourites">Favoritter</string> + <string name="action_view_mutes">Dempede brukere</string> + <string name="action_view_blocks">Blokkerte brukere</string> + <string name="action_view_follow_requests">Følgeforespørsler</string> + <string name="action_view_media">Media</string> + <string name="action_open_in_web">Åpne i nettleser</string> + <string name="action_add_media">Legg til media</string> + <string name="action_photo_take">Ta bilde</string> + <string name="action_share">Del</string> + <string name="action_mute">Demp</string> + <string name="action_unmute">Fjern demping</string> + <string name="action_mention">Nevn</string> + <string name="action_hide_media">Skjul media</string> + <string name="action_open_drawer">Åpne skuff</string> + <string name="action_save">Lagre</string> + <string name="action_edit_profile">Endre profil</string> + <string name="action_edit_own_profile">Endre</string> + <string name="action_undo">Angre</string> + <string name="action_accept">Aksepter</string> + <string name="action_reject">Avvis</string> + <string name="action_search">Søk</string> + <string name="action_access_drafts">Utkast</string> + <string name="action_toggle_visibility">Synlighet på innlegg</string> + <string name="action_content_warning">Innholdsadvarsel</string> + <string name="action_emoji_keyboard">Emoji-tastatur</string> + <string name="action_add_tab">Legg til fane</string> + <string name="action_links">Lenker</string> + <string name="action_mentions">Nevnelser</string> + <string name="action_hashtags">Emneknagger</string> + <string name="action_open_reblogger">Åpne deler</string> + <string name="action_open_reblogged_by">Vis delinger</string> + <string name="action_open_faved_by">Vis favoritter</string> + <string name="title_hashtags_dialog">Stikkord</string> + <string name="title_mentions_dialog">Nevnelser</string> + <string name="title_links_dialog">Lenker</string> + <string name="action_open_media_n">Åpne media #%1$d</string> + <string name="download_image">Laster ned %1$s</string> + <string name="action_copy_link">Kopier lenken</string> + <string name="action_open_as">Åpne som %1$s</string> + <string name="action_share_as">Del som …</string> + <string name="download_media">Last ned media</string> + <string name="downloading_media">Laster ned media</string> + <string name="send_post_link_to">Del tut-URL til…</string> + <string name="send_post_content_to">Del tut til…</string> + <string name="send_media_to">Del media til…</string> + <string name="confirmation_reported">Sendt!</string> + <string name="confirmation_unblocked">Fjernet blokkering av bruker</string> + <string name="confirmation_unmuted">Fjernet demping av bruker</string> + <string name="hint_domain">Hvilken instanse\?</string> + <string name="hint_compose">Hva skjer\?</string> + <string name="hint_content_warning">Innholdsadvarsel</string> + <string name="hint_display_name">Visningsnavn</string> + <string name="hint_note">Biografi</string> + <string name="hint_search">Søk…</string> + <string name="search_no_results">Ingen resultater</string> + <string name="label_quick_reply">Svar…</string> + <string name="label_avatar">Avatar</string> + <string name="label_header">Overskrift</string> + <string name="link_whats_an_instance">Hva er en instans\?</string> + <string name="login_connection">Kobler til…</string> + <string name="dialog_whats_an_instance">Addressen eller domenet til enhver instans kan legges til her, f. eks. mastodon.social, icosahedron.website, social.tchncs.de og <a href="https://instances.social">mer!</a> +\n +\nHvis du ikke har en konto kan du oppgi navnet på instansen du vil bli medlem av, og lage en konto der. +\n +\nEn instans der hvor du har kontoen din, men du kan lett kommunisere med og følge andre personer på andre instanser, som om du var på den samme nettsiden. +\n +\nDu kan finne mer info på <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="dialog_title_finishing_media_upload">Opplasting av media er ferdig</string> + <string name="dialog_message_uploading_media">Laster opp…</string> + <string name="dialog_download_image">Last ned</string> + <string name="dialog_message_cancel_follow_request">Trekk tilbake følgeforespørselen\?</string> + <string name="dialog_unfollow_warning">Slutt å følge denne kontoen\?</string> + <string name="dialog_delete_post_warning">Slett dette innlegget\?</string> + <string name="visibility_public">Offentlig: Vis i offentlige tidslinjer</string> + <string name="visibility_unlisted">Ikke oppført: Ikke vis i offentlige tidslinjer</string> + <string name="visibility_private">Bare følgere: Vis bare til følgere</string> + <string name="visibility_direct">Direkte: Vis bare til nevnte brukere</string> + <string name="pref_title_edit_notification_settings">Varsler</string> + <string name="pref_title_notifications_enabled">Varsler</string> + <string name="pref_title_notification_alerts">Varsling</string> + <string name="pref_title_notification_alert_sound">Varsle med lyd</string> + <string name="pref_title_notification_alert_vibrate">Varsle med vibrasjon</string> + <string name="pref_title_notification_alert_light">Varsle med lys</string> + <string name="pref_title_notification_filters">Varsle meg når</string> + <string name="pref_title_notification_filter_mentions">jeg blir nevnt</string> + <string name="pref_title_notification_filter_follows">jeg blir fulgt</string> + <string name="pref_title_notification_filter_reblogs">innleggene mine blir delt</string> + <string name="pref_title_notification_filter_favourites">innleggene mine blir favorisert</string> + <string name="pref_title_appearance_settings">Utseende</string> + <string name="pref_title_app_theme">Apptema</string> + <string name="pref_title_timelines">Tidslinjer</string> + <string name="pref_title_timeline_filters">Filtere</string> + <string name="pref_title_browser_settings">Nettleser</string> + <string name="pref_title_custom_tabs">Bruk Chrome-tilpassede faner</string> + <string name="pref_title_language">Språk</string> + <string name="pref_title_post_filter">Tidslinjefiltrering</string> + <string name="pref_title_post_tabs">Faner</string> + <string name="pref_title_show_boosts">Vis delinger</string> + <string name="pref_title_show_replies">Vis svar</string> + <string name="pref_title_show_media_preview">Last ned forhåndsvisning av media</string> + <string name="pref_title_proxy_settings">Proxy</string> + <string name="pref_title_http_proxy_settings">HTTP-proxy</string> + <string name="pref_title_http_proxy_enable">Skru på HTTP-proxy</string> + <string name="pref_title_http_proxy_server">HTTP-proxyserver</string> + <string name="pref_title_http_proxy_port">HTTP-proxyport</string> + <string name="pref_default_media_sensitivity">Marker alltid media som sensitivt</string> + <string name="pref_publishing">Publiserer (synkronisert med server)</string> + <string name="pref_failed_to_sync">Synkronisering av innstillinger feilet</string> + <string name="pref_post_text_size">Størrelse på statustekst</string> + <string name="notification_follow_name">Nye følgere</string> + <string name="notification_follow_description">Varsler om nye følgere</string> + <string name="notification_boost_name">Delte innlegg</string> + <string name="notification_boost_description">Varsler når innleggene dine blir delt</string> + <string name="notification_favourite_name">Favoritter</string> + <string name="notification_favourite_description">Varsler når innleggene dine blir favorisert</string> + <string name="notification_mention_format">%1$s nevnte deg</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s og %4$d andre</string> + <string name="notification_summary_medium">%1$s, %2$s, og %3$s</string> + <string name="notification_summary_small">%1$s og %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d ny interaksjon</item> + <item quantity="other">%1$d nye interaksjoner</item> + </plurals> + <string name="description_account_locked">Låst konto</string> + <string name="about_title_activity">Om</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_bug_feature_request_site">Rapporter feil og ønsker om funksjonalitet her: +\n https://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Tuskys Profil</string> + <string name="post_share_content">Del inneholdet i innlegget</string> + <string name="post_share_link">Del lenke til tuten</string> + <string name="post_media_images">Bilder</string> + <string name="post_media_video">Video</string> + <string name="state_follow_requested">Forespørsel sendt</string> + <string name="abbreviated_in_days">om %1$dd</string> + <string name="abbreviated_in_hours">om %1$dh</string> + <string name="abbreviated_in_minutes">om %1$dm</string> + <string name="abbreviated_in_seconds">om %1$ds</string> + <string name="abbreviated_years_ago">%1$dy</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="follows_you">Følger deg</string> + <string name="pref_title_alway_show_sensitive_media">Vis alltid sensitivt innhold</string> + <string name="title_media">Media</string> + <string name="replying_to">Svarer til @%1$s</string> + <string name="load_more_placeholder_text">last mer</string> + <string name="pref_title_public_filter_keywords">Offentlige tidslinjer</string> + <string name="pref_title_thread_filter_keywords">Samtaler</string> + <string name="filter_addition_title">Legg til filter</string> + <string name="filter_edit_title">Endre filter</string> + <string name="filter_dialog_remove_button">Fjern</string> + <string name="filter_dialog_update_button">Oppdater</string> + <string name="filter_add_description">Filtrer frase</string> + <string name="add_account_name">Legg til konto</string> + <string name="add_account_description">Legg til ny Mastodon-konto</string> + <string name="action_lists">Lister</string> + <string name="title_lists">Lister</string> + <string name="error_create_list">Kunne ikke opprette liste</string> + <string name="error_rename_list">Kunne ikke gi liste nytt navn</string> + <string name="error_delete_list">Kunne ikke slette liste</string> + <string name="action_create_list">Opprett en liste</string> + <string name="action_rename_list">Gi listen nytt navn</string> + <string name="action_delete_list">Fjern listen</string> + <string name="hint_search_people_list">Søk etter personer du følger</string> + <string name="action_add_to_list">Legg til konto i listen</string> + <string name="action_remove_from_list">Fjern konto fra listen</string> + <string name="pref_default_post_privacy">Standardinnstilling for innlegg</string> + <string name="notification_mention_name">Nye nevnelser</string> + <string name="notification_mention_descriptions">Varsler om nye nevnelser</string> + <string name="about_tusky_license">Tusky er fri og åpen kildekode. Applikasjonen er lisensiert under GNU General Public License versjon 3. Du kan se lisensen her: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="about_project_site">Hjemmeside: +\n https://tusky.app</string> + <string name="abbreviated_in_years">om %1$dy</string> + <string name="compose_active_account_description">Poster som %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Beskriv innhold for de med nedsatt synsevne +\n(maks %1$d tegn)</item> + <item quantity="other">Beskriv innhold for de med nedsatt synsevne +\n(maks %1$d tegn)</item> + </plurals> + <string name="action_set_caption">Sett bildetekst</string> + <string name="action_remove">Slett</string> + <string name="lock_account_label">Lås konto</string> + <string name="lock_account_label_description">Krever at du manuelt godkjenner nye følgere</string> + <string name="compose_save_draft">Lagre utkast\?</string> + <string name="send_post_notification_title">Sender innlegg…</string> + <string name="send_post_notification_error_title">Det oppsto en feil under sending av innlegget</string> + <string name="send_post_notification_channel_name">Sender innleggene</string> + <string name="send_post_notification_cancel_title">Sending avbrutt</string> + <string name="send_post_notification_saved_content">En kopi av innlegget er lagret i utkastene dine</string> + <string name="action_compose_shortcut">Skriv</string> + <string name="error_no_custom_emojis">Instansen %1$s har ingen egendefinerte emojis</string> + <string name="emoji_style">Emoji-stil</string> + <string name="system_default">Systemstandard</string> + <string name="download_fonts">Du må laste ned emoji-samlingene før de kan brukes</string> + <string name="performing_lookup_title">Søker…</string> + <string name="expand_collapse_all_posts">Utvid/Gjem alle statuser</string> + <string name="action_open_post">Åpne tut</string> + <string name="restart_required">Omstart av applikasjonen kreves</string> + <string name="restart_emoji">Du må starte Tusky på nytt for at endringene blir aktivert</string> + <string name="later">Senere</string> + <string name="restart">Start på nytt</string> + <string name="caption_systememoji">Standard-emojis for din enhet</string> + <string name="caption_blobmoji">Blob-emojis kjent fra Android 4.4–7.1</string> + <string name="caption_twemoji">Mastadons standard emoji-samling</string> + <string name="download_failed">Nedlasting feilet</string> + <string name="profile_badge_bot_text">Robot</string> + <string name="account_moved_description">%1$s har flyttet til:</string> + <string name="reblog_private">Del til opprinnelig publikum</string> + <string name="unreblog_private">Fjern deling</string> + <string name="license_description">Tusky inneholder programkode og elementer fra følgende åpen kildekode-prosjekter:</string> + <string name="license_apache_2">Lisensiert under Apache License (kopi under)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Profilmetadata</string> + <string name="profile_metadata_add">legg til data</string> + <string name="profile_metadata_label_label">Ledetekst</string> + <string name="profile_metadata_content_label">Innhold</string> + <string name="pref_title_absolute_time">Bruk absolutt tid</string> + <string name="label_remote_account">Informasjonen under kan være mangelfull. Trykk for å åpne komplett brukerprofil i nettleseren.</string> + <string name="unpin_action">Fjern feste</string> + <string name="pin_action">Fest</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Favoritt</item> + <item quantity="other"><b>%1$s</b> Favoritter</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one">Delt <b>%1$s</b> gang</item> + <item quantity="other">Delt <b>%1$s</b> ganger</item> + </plurals> + <string name="title_reblogged_by">Delt av</string> + <string name="title_favourited_by">Favorisert av</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s og %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s og %3$d fler</string> + <string name="description_post_media">Media: %1$s</string> + <string name="description_post_cw">Innholdsadvarsel: %1$s</string> + <string name="description_post_media_no_description_placeholder">Ingen beskrivelse</string> + <string name="description_post_reblogged">Reblogget</string> + <string name="description_post_favourited">Favorisert</string> + <string name="description_visibility_public">Offentlig</string> + <string name="description_visibility_unlisted">Ikke listet</string> + <string name="description_visibility_private">Følgere</string> + <string name="description_visibility_direct">Direkte</string> + <string name="hint_list_name">Listenavn</string> + <string name="edit_hashtag_hint">Emneord uten #</string> + <string name="notifications_clear">Fjern</string> + <string name="notifications_apply_filter">Filter</string> + <string name="filter_apply">Bruk</string> + <string name="compose_shortcut_long_label">Skriv innlegg</string> + <string name="compose_shortcut_short_label">Skriv</string> + <string name="pref_title_bot_overlay">Vis robotindikator</string> + <string name="notification_clear_text">Er du sikker på at du vil slette alle varsler\?</string> + <string name="action_delete_and_redraft">Slett og skriv på nytt</string> + <string name="dialog_redraft_post_warning">Vil du slette denne tuten og skrive den på nytt\?</string> + <string name="poll_info_format"> <!-- 15 votes • 1 hour left --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s stemme</item> + <item quantity="other">%1$s stemmer</item> + </plurals> + <string name="poll_info_time_absolute">avsluttes %1$s</string> + <string name="poll_info_closed">stengt</string> + <string name="poll_vote">Stem</string> + <string name="app_them_dark">Mørk</string> + <string name="app_theme_light">Lys</string> + <string name="app_theme_black">Svart</string> + <string name="app_theme_auto">Automatisk ved solnedgang</string> + <string name="app_theme_system">Bruk systeminnstillinger</string> + <string name="post_privacy_public">Offentlig</string> + <string name="post_privacy_unlisted">Ikke listet</string> + <string name="post_privacy_followers_only">Kun følgere</string> + <string name="post_text_size_smallest">Minste</string> + <string name="post_text_size_small">Liten</string> + <string name="post_text_size_medium">Medium</string> + <string name="post_text_size_large">Stor</string> + <string name="post_text_size_largest">Størst</string> + <string name="pref_title_notification_filter_poll">Avstemminger er avsluttet</string> + <string name="notification_poll_name">Avstemminger</string> + <string name="notification_poll_description">Varsler om avstemminger som er avsluttet</string> + <string name="poll_ended_voted">En avstemming du har stemt på er avsluttet</string> + <string name="poll_ended_created">En avstemming du opprettet er avsluttet</string> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d dag igjen</item> + <item quantity="other">%1$d dager igjen</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d time igjen</item> + <item quantity="other">%1$d timer igjen</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d minutt igjen</item> + <item quantity="other">%1$d minutter igjen</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d sekund igjen</item> + <item quantity="other">%1$d sekunder igjen</item> + </plurals> + <string name="compose_preview_image_description">Handlinger for bilde %1$s</string> + <string name="pref_title_animate_gif_avatars">Animer GIF-avatarer</string> + <string name="description_poll">Avstemming med valg: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="caption_notoemoji">Googles nåværende emoji-samling</string> + <string name="button_continue">Fortsett</string> + <string name="button_back">Tilbake</string> + <string name="button_done">Ferdig</string> + <string name="report_sent_success">\@%1$s er rapportert</string> + <string name="hint_additional_info">Flere kommentarer</string> + <string name="report_remote_instance">Videresend til %1$s</string> + <string name="failed_report">Klarte ikke å rapportere</string> + <string name="failed_fetch_posts">Klarte ikke å hente innlegg</string> + <string name="report_description_1">Rapporten vil bli sendt til instansmoderatoren. Under kan du skrive en forklaring på hvorfor du rapporterer denne kontoen:</string> + <string name="report_description_remote_instance">Kontoen tilhører en annen instans. Vil du også sende en anonymisert kopi av rapporten dit\?</string> + <string name="title_domain_mutes">Skjulte domener</string> + <string name="action_view_domain_mutes">Skjulte domener</string> + <string name="action_mute_domain">Demp %1$s</string> + <string name="confirmation_domain_unmuted">%1$s er ikke lenger skjult</string> + <string name="mute_domain_warning">Er du sikker på at du vil blokkere alt fra %1$s\? Du kommer ikke til å se innhold fra domenet i noen offentlige tidslinjer, eller i varslene dine. Kontoer som følger deg fra dette domenet vil bli fjernet.</string> + <string name="mute_domain_warning_dialog_ok">Skjul hele domenet</string> + <string name="filter_dialog_whole_word">Helt ord</string> + <string name="filter_dialog_whole_word_description">Når nøkkelordet eller frasen kun inneholder bokstaver og tall, vil det bare brukes dersom det stemmer overens med hele ordet</string> + <string name="title_accounts">Kontoer</string> + <string name="failed_search">Klarte ikke å søke</string> + <string name="pref_title_alway_open_spoiler">Ekspander alltid innlegg markert med innholdsadvarsel</string> + <string name="action_add_poll">Legg til avstemming</string> + <string name="create_poll_title">Avstemming</string> + <string name="duration_5_min">5 minutter</string> + <string name="duration_30_min">30 minutter</string> + <string name="duration_1_hour">1 time</string> + <string name="duration_6_hours">6 timer</string> + <string name="duration_1_day">1 dag</string> + <string name="duration_3_days">3 dager</string> + <string name="duration_7_days">7 dager</string> + <string name="add_poll_choice">Legg til valg</string> + <string name="poll_allow_multiple_choices">Flere valg</string> + <string name="poll_new_choice_hint">Valg %1$d</string> + <string name="edit_poll">Endre</string> + <string name="title_scheduled_posts">Planlagte innlegg</string> + <string name="action_edit">Rediger</string> + <string name="action_access_scheduled_posts">Planlagte innlegg</string> + <string name="action_schedule_post">Planlegg innlegg</string> + <string name="action_reset_schedule">Tilbakestill</string> + <string name="post_lookup_error_format">Det oppsto en feil under henting av %1$s</string> + <string name="about_powered_by_tusky">Drevet av Tusky</string> + <string name="title_bookmarks">Bokmerker</string> + <string name="action_bookmark">Bokmerke</string> + <string name="action_view_bookmarks">Bokmerker</string> + <string name="description_post_bookmarked">Bokmerke lagt til</string> + <string name="select_list_title">Velg liste</string> + <string name="list">Liste</string> + <string name="no_scheduled_posts">Du har ingen planlagte innlegg.</string> + <string name="no_drafts">Du har ikke lagret noen utkast.</string> + <string name="warning_scheduling_interval">Mastodon har et minimums planleggingsinterval på 5 minutter.</string> + <string name="pref_title_show_cards_in_timelines">Vis forhåndsvisning av linker i tidslinjer</string> + <string name="pref_title_confirm_reblogs">Vis bekreftelser før deling</string> + <string name="pref_title_enable_swipe_for_tabs">Skru på sveiping for å bytte mellom faner</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s person</item> + <item quantity="other">%1$s personer</item> + </plurals> + <string name="notification_follow_request_description">Varsler om følgeforespørsler</string> + <string name="notification_follow_request_name">Følgeforespørsler</string> + <string name="pref_title_notification_filter_follow_requests">Følgeforespørsel sendt</string> + <string name="dialog_mute_warning">Dempe @%1$s\?</string> + <string name="dialog_block_warning">Blokkere @%1$s\?</string> + <string name="action_unmute_conversation">Fjern demping av samtale</string> + <string name="action_mute_conversation">Demp samtale</string> + <string name="hashtags">Stikkord</string> + <string name="add_hashtag_title">Legg til stikkord</string> + <string name="notification_follow_request_format">%1$s ønsker å følge deg</string> + <string name="pref_title_gradient_for_media">Vis fargerike gradienter for skjult media</string> + <string name="pref_main_nav_position_option_bottom">Bunn</string> + <string name="pref_main_nav_position_option_top">Topp</string> + <string name="pref_main_nav_position">Plassering av hovednavigasjon</string> + <string name="action_unmute_domain">Fjern demping av %1$s</string> + <string name="dialog_mute_hide_notifications">Skjul varsler</string> + <string name="action_unmute_desc">Fjern demping av %1$s</string> + <string name="pref_title_hide_top_toolbar">Skjul tittelen på den øverste verktøylinjen</string> + <string name="account_note_saved">Lagret!</string> + <string name="account_note_hint">Ditt private notat om denne kontoen</string> + <string name="no_announcements">Det er ingen kunngjøringer.</string> + <string name="title_announcements">Kunngjøringer</string> + <string name="wellbeing_hide_stats_profile">Skjul kvantitativ informasjon på profiler</string> + <string name="wellbeing_hide_stats_posts">Skjul kvantitativ statistikk på innleggene</string> + <string name="limit_notifications">Begrens tidslinjevarsler</string> + <string name="review_notifications">Se over varsler</string> + <string name="wellbeing_mode_notice">Informasjon som kan påvirke ditt mentale velvære vil bli skjult. Dette inkluderer: +\n +\n - Varsler om favorisering, deling og følgere +\n - Antall favoriseringer og delinger av innlegg +\n - Antall følgere og innlegg på profiler +\n +\n Push-varsler vil ikke påvirkes, men du kan se over dine varselinnstillinger manuelt.</string> + <string name="pref_title_wellbeing_mode">Velvære</string> + <string name="notification_subscription_description">Varsler når noen jeg følger publiserer et nytt innlegg</string> + <string name="notification_subscription_name">Nye innlegg</string> + <string name="pref_title_notification_filter_subscriptions">noen jeg følger publiserer et nytt innlegg</string> + <string name="notification_subscription_format">%1$s postet akkurat</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Du kan ikke laste opp flere enn %1$d mediavedlegg.</item> + <item quantity="other">Du kan ikke laste opp flere enn %1$d mediavedlegg.</item> + </plurals> + <string name="duration_indefinite">Uendelig</string> + <string name="label_duration">Varighet</string> + <string name="dialog_delete_list_warning">Er du sikker på at du vil slette listen %1$s\?</string> + <string name="post_media_attachments">Vedlegg</string> + <string name="post_media_audio">Lyd</string> + <string name="drafts_post_reply_removed">Innlegget du hadde opprettet et utkast som svar på har blitt fjernet</string> + <string name="draft_deleted">Utkast slettet</string> + <string name="drafts_failed_loading_reply">Lasting av svarinformasjon feilet</string> + <string name="drafts_post_failed_to_send">Sending av innlegg feilet!</string> + <string name="pref_title_animate_custom_emojis">Animer egendefinerte emojis</string> + <string name="action_unsubscribe_account">Avslutt abonnementet</string> + <string name="action_subscribe_account">Abonner</string> + <string name="follow_requests_info">Selv om kontoen din ikke er låst, har %1$s administratorer markert disse følgeforespørsler for manuell godkjenning.</string> + <string name="dialog_delete_conversation_warning">Slett denne samtalen\?</string> + <string name="action_delete_conversation">Slett samtale</string> + <string name="action_unbookmark">Fjern bokmerke</string> + <string name="pref_title_confirm_favourites">Vis bekreftelser når favoritt skal legges til</string> + <string name="duration_30_days">30 dager</string> + <string name="duration_60_days">60 dager</string> + <string name="duration_90_days">90 dager</string> + <string name="duration_180_days">180 dager</string> + <string name="duration_365_days">365 dager</string> + <string name="duration_14_days">14 dager</string> + <string name="tusky_compose_post_quicksetting_label">Skriv innlegg</string> + <string name="notification_sign_up_format">%1$s registrerte seg</string> + <string name="pref_title_notification_filter_sign_ups">noen registrerte seg</string> + <string name="notification_sign_up_name">Registreringer</string> + <string name="notification_sign_up_description">Varslinger om nye brukere</string> + <string name="notification_update_format">%1$s redigerte innlegget sitt</string> + <string name="pref_title_notification_filter_updates">et innlegg jeg har interagert med, er redigert</string> + <string name="notification_update_name">Redigerte innlegg</string> + <string name="notification_update_description">Varslinger når et innlegg du har interagert med er redigert</string> + <string name="title_login">Logg inn</string> + <string name="error_could_not_load_login_page">Klarte ikke å laste innloggingssiden.</string> + <string name="title_migration_relogin">Logg inn på nytt for pushvarsler</string> + <string name="action_dismiss">Avvis</string> + <string name="action_details">Detaljer</string> + <string name="account_date_joined">Ble med %1$s</string> + <string name="tips_push_notification_migration">Logg inn all konti på nytt for å skru på pushvarsler.</string> + <string name="dialog_push_notification_migration">For å kunne sende pushvarsler via UnifiedPush trenger Tusky tillatelse til å abonnere på varsler på Mastodon-serveren. Dette krever at du logger inn på nytt. Ved å bruke muligheten til å logge inn på nytt her eller i kontoinstillinger vil alle lokale utkast være tilgjengelig også etter at du har logget inn på nytt.</string> + <string name="dialog_push_notification_migration_other_accounts">Du har logget inn på nytt for å tillate Tusky til å sende pushvarsler, men du har fortsatt andre konti som ikke har fått den nødvendige tillatelsen. Bytt til dem og logg inn på nytt på samme måte for å skru på støtte for pushvarsler via UnifiedPush.</string> + <string name="saving_draft">Lagrer utkast…</string> + <string name="status_count_one_plus">1+</string> + <string name="action_edit_image">Rediger bilde</string> + <string name="error_image_edit_failed">Bildet kunne ikke redigeres.</string> + <string name="error_loading_account_details">Lasting av kontodetaljer feilet</string> + <string name="error_multimedia_size_limit">Video- og lydfiler kan ikke være større enn %1$s MB.</string> + <string name="error_following_hashtag_format">Det oppsto en feil under følging av #%1$s</string> + <string name="error_unfollowing_hashtag_format">Kunne ikke slutte å følge #%1$s</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="duration_no_change">(Ingen endring)</string> + <string name="description_post_language">Innleggspråk</string> + <string name="action_set_focus">Sett fokuspunkt</string> + <string name="set_focus_description">Trykk eller dra sirkelen for å velge fokuspunktet som alltid skal være synlig i miniatyrbilder.</string> + <string name="pref_show_self_username_always">Alltid</string> + <string name="pref_show_self_username_disambiguate">Når flere konti er logget inn</string> + <string name="pref_show_self_username_never">Aldri</string> + <string name="pref_title_show_self_username">Vis brukernavn på verktøylinjer</string> + <string name="delete_scheduled_post_warning">Slette dette planlagte innlegget\?</string> + <string name="instance_rule_title">Regler på %1$s</string> + <string name="instance_rule_info">Ved å logge inn godtar du reglene på %1$s.</string> + <string name="compose_save_draft_loses_media">Lagre utkast\? (Vedlegg vil bli lastet opp igjen når du fortsetter å jobbe på utkastet.)</string> + <string name="failed_to_pin">Klarte ikke å feste</string> + <string name="failed_to_unpin">Klarte ikke å løsne</string> + <string name="action_add_reaction">Legg til reaksjon</string> + <string name="action_add_or_remove_from_list">Legg til eller fjern fra liste</string> + <string name="failed_to_add_to_list">Klarte ikke å legge til kontoen til listen</string> + <string name="failed_to_remove_from_list">Klarte ikke å fjerne kontoen fra listen</string> + <string name="pref_default_post_language">Standardspråk på innlegg</string> + <string name="no_lists">Du har ingen lister.</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="status_created_at_now">nå</string> + <string name="post_edited">Redigert %1$s</string> + <string name="notification_summary_report_format">%1$s · %2$d innlegg vedlagt</string> + <string name="notification_report_format">Ny rapport på %1$s</string> + <string name="notification_header_report_format">%1$s rapporterte %2$s</string> + <string name="notification_report_description">Varsler om moderasjonsrapporter</string> + <string name="pref_title_notification_filter_reports">det er en ny rapport</string> + <string name="notification_report_name">Rapporter</string> + <string name="description_post_edited">Redigert</string> + <string name="error_following_hashtags_unsupported">Denne instansen støtter ikke følging av emneknagger.</string> + <string name="title_followed_hashtags">Fulgte Emneknagger</string> + <string name="report_category_violation">Regelbrudd</string> + <string name="report_category_spam">Spam</string> + <string name="report_category_other">Annet</string> + <string name="action_unfollow_hashtag_format">Slutte å følge #%1$s\?</string> + <string name="confirmation_hashtag_unfollowed">Sluttet å følge #%1$s</string> + <string name="a11y_label_loading_thread">Laster tråd</string> + <string name="help_empty_home">Dette er <b>Hjemmetidslinjen</b>din. Den viser de nyeste innleggene av kontoer du følger. +\n +\nFor å utforske kontoer kan du enten oppdage dem i en av tidslinjene dine, for eksempel den lokale tidslinjen for instansen din [iconics gmd_group], eller du kan søke etter navnet deres [iconics gmd_search]; søk for eksempel etter Tusky før å finne Mastodonkonten vår.</string> + <string name="pref_title_reading_order">Leserekkefølge</string> + <string name="pref_reading_order_oldest_first">Eldste først</string> + <string name="pref_ui_text_size">Skriftstørrelse for brukergrensesnitt</string> + <string name="pref_reading_order_newest_first">Nyeste først</string> + <string name="dialog_follow_hashtag_title">Følg emneknagger</string> + <string name="dialog_follow_hashtag_hint">#emneknagg</string> + <string name="accessibility_talking_about_tag">%1$d folk snakker om emneknaggen %2$s</string> + <string name="total_usage">Total bruk</string> + <string name="total_accounts">Totale kontoer</string> + <string name="pref_summary_http_proxy_missing"><ikke satt></string> + <string name="pref_summary_http_proxy_disabled">Deaktivert</string> + <string name="pref_summary_http_proxy_invalid"><ugyldig></string> + <string name="notification_listenable_worker_name">Bakgrunnsaktivitet</string> + <string name="notification_listenable_worker_description">Varsler når Tusky arbeider i bakgrunnen</string> + <string name="notification_notification_worker">Henter varsler…</string> + <string name="notification_prune_cache">Cache-vedlikehold…</string> + <string name="mute_notifications_switch">Demp varsler</string> + <string name="title_edits">Endringer</string> + <string name="status_created_info">Skapt: %1$s</string> + <string name="status_edit_info">Endret: %1$s</string> + <string name="hint_media_description_missing">Media bør ha en beskrivelse.</string> + <string name="action_refresh">Oppdater</string> + <string name="post_media_image">Bilde</string> + <string name="ui_error_bookmark">Å bokmerke innlegget misslyktes: %1$s</string> + <string name="hint_filter_title">Mitt filter</string> + <string name="filter_edit_keyword_title">Endre nøkkelord</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="pref_title_http_proxy_port_message">Portnummer burde være mellom %1$d og %2$d</string> + <string name="action_post_failed">Opplastningen mislykkets</string> + <string name="action_post_failed_detail_plural">Opplastningen av innleggene dine mislykktes og de har blitt lagret som utkast. +\n +\nEnten kunne ikke tjeneren nås, eller så ble opplastningen nektet.</string> + <string name="action_post_failed_show_drafts">Vis utkast</string> + <string name="action_post_failed_do_nothing">Lukk</string> + <string name="post_media_alt">ALT</string> + <string name="notification_unknown_name">Ukjent</string> + <string name="socket_timeout_exception">Det tok for lang tid å kontaktere tjeneren din</string> + <string name="ui_error_clear_notifications">Sletting av vasler feilet: %1$s</string> + <string name="ui_error_reject_follow_request">Avvisning av følgeforespørselen misslyktes: %1$s</string> + <string name="ui_error_accept_follow_request">Godtagelse av følgeforespørsel mislykktes: %1$s</string> + <string name="error_muting_hashtag_format">Kunne ikke dempe #%1$s</string> + <string name="error_unmuting_hashtag_format">Kunne ikke skru av dempingen av #%1$s</string> + <string name="action_browser_login">Logg inn med nettleseren</string> + <string name="description_browser_login">Kan støtte flere autentiseringsmetoder, men trenger en støttet nettleser.</string> + <string name="description_login">Virker som oftest. Ingen informasjon deles med andre apper.</string> + <string name="action_post_failed_detail">Opplastingen misslykktes og har blit lagret som utkast. +\n +\nEnten kunne tjeneren ikke kontakteres, eller den nektet opplastningen.</string> + <string name="ui_success_accepted_follow_request">Følgeforespørsel akseptert</string> + <string name="action_discard">Forkast endringer</string> + <string name="action_continue_edit">Fortsett endring</string> + <string name="compose_unsaved_changes">Du har ulagrede endringer.</string> + <string name="select_list_manage">Forvalte lister</string> + <string name="ui_error_reblog">Deling av innlegget feilet: %1$s</string> + <string name="ui_error_unknown">ukjent grunn</string> + <string name="ui_error_favourite">Favorisering av innlegg feilet: %1$s</string> + <string name="ui_error_vote">Stemming mislykkes: %1$s</string> + <string name="filter_description_hide">Gjem helt</string> + <string name="label_filter_action">Filterhandling</string> + <string name="filter_action_hide">Gjem</string> + <string name="filter_description_warn">Gjem med et varsel</string> + <string name="action_share_account_link">Del link til profil</string> + <string name="action_share_account_username">Del brukernavnet til kontoen</string> + <string name="send_account_link_to">Del konto-URL med…</string> + <string name="send_account_username_to">Del kontoens brukernavn med…</string> + <string name="account_username_copied">Brukernavn kopiert</string> + <string name="error_status_source_load">Kunne ikke laste status fra tjeneren.</string> + <string name="status_filtered_show_anyway">Vis allikevel</string> + <string name="status_filter_placeholder_label_format">Filtrert: %1$s</string> + <string name="pref_title_account_filter_keywords">Profiler</string> + <string name="action_add">Legg til</string> + <string name="filter_keyword_display_format">%1$s (helt ord)</string> + <string name="label_filter_context">Filtrer sammenhenger</string> + <string name="label_filter_keywords">Nøkkelord eller fraser som filtreres</string> + <string name="ui_success_rejected_follow_request">Følgeforespørsel blokkert</string> + <string name="label_filter_title">Tittel</string> + <string name="filter_action_warn">Varsle</string> + <string name="filter_keyword_addition_title">Legg til nøkkelord</string> + <string name="title_public_trending_hashtags">Trendy hashtaggs</string> + <string name="pref_title_show_stat_inline">Vis poststatistikk i tidslinjen</string> + <string name="load_newest_notifications">Last de nyeste varslene</string> + <string name="compose_delete_draft">Del utkast\?</string> + <string name="error_missing_edits">Serveren din vet at denne posten ble endret, men har ingen kopi av entringene så de kann ikke vises. +\n +\nDette er <a href="https://github.com/mastodon/mastodon/issues/25398">Mastodon issue #25398</a>.</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-night/theme_colors.xml b/app/src/main/res/values-night/theme_colors.xml new file mode 100644 index 0000000..fcf044e --- /dev/null +++ b/app/src/main/res/values-night/theme_colors.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="colorPrimary">@color/tusky_blue_light</color> + <color name="colorSecondary">@color/tusky_blue_light</color> + <color name="colorSurface">@color/tusky_grey_30</color> + <color name="colorPrimaryDark">@color/tusky_grey_25</color> + + <color name="colorOnPrimary">@color/tusky_grey_10</color> + + <color name="colorBackground">@color/tusky_grey_20</color> + <color name="windowBackground">@color/tusky_grey_10</color> + + <color name="textColorPrimary">@color/white</color> + <color name="textColorSecondary">@color/tusky_grey_90</color> + <color name="textColorTertiary">@color/tusky_grey_70</color> + <color name="textColorDisabled">@color/tusky_grey_40</color> + + <color name="iconColor">@color/tusky_grey_70</color> + + <color name="colorBackgroundAccent">@color/tusky_grey_30</color> + <color name="colorBackgroundHighlight">@color/tusky_grey_50</color> + <color name="dividerColor">@color/tusky_grey_30</color> + <color name="dividerColorOther">@color/tusky_grey_10</color> + + <color name="favoriteButtonActiveColor">@color/tusky_orange</color> + + <color name="warning_color">@color/tusky_red_lighter</color> + + <color name="headerBackgroundFilter">@color/header_background_filter_dark</color> + + <bool name="lightNavigationBar">false</bool> + + <color name="botBadgeForeground">@color/white</color> + <color name="botBadgeBackground">@color/tusky_grey_10</color> + + <color name="toolbar_icon_background">#CC444B5D</color> + + <!-- colors used to show inserted/deleted text --> + <color name="view_edits_background_insert">@color/tusky_green</color> + <color name="view_edits_background_delete">@color/tusky_red</color> +</resources> diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml new file mode 100644 index 0000000..4a70881 --- /dev/null +++ b/app/src/main/res/values-nl/strings.xml @@ -0,0 +1,659 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Er deed zich een fout voor.</string> + <string name="error_network">Er deed zich een netwerkfout voor. Controleer je verbinding en probeer opnieuw.</string> + <string name="error_empty">Dit mag niet leeg zijn.</string> + <string name="error_invalid_domain">Ongeldige domeinnaam ingevoerd</string> + <string name="error_failed_app_registration">Authenticatie met die server is mislukt. Als het probleem blijft, probeer dan de browser login via het menu.</string> + <string name="error_no_web_browser_found">Kon geen bruikbare webbrowser vinden.</string> + <string name="error_authorization_unknown">Er deed zich een onbekende autorisatiefout voor. Als dit blijft, probeer dan “ Via de browser inloggen” in het menu.</string> + <string name="error_authorization_denied">Autorisatie werd geweigerd. Als u zeker weet dat u de correcte gegevens heeft ingevoerd, probeer dan in te loggen in de browser via het menu.</string> + <string name="error_retrieving_oauth_token">Kon geen inlogsleutel verkrijgen. Als het probleem zich blijft herhalen; probeer dan de browser login via menu.</string> + <string name="error_compose_character_limit">Tekst van dit bericht is te lang!</string> + <string name="error_media_upload_type">Bestandstype wordt niet ondersteund.</string> + <string name="error_media_upload_opening">Bestand kon niet worden geopend.</string> + <string name="error_media_upload_permission">Er is toestemming nodig om deze media te lezen.</string> + <string name="error_media_download_permission">Er is toestemming nodig om media op te slaan.</string> + <string name="error_media_upload_image_or_video">Afbeeldingen en video\'s kunnen niet samen aan hetzelfde bericht worden toegevoegd.</string> + <string name="error_media_upload_sending">Uploaden mislukt.</string> + <string name="error_sender_account_gone">Fout tijdens verzenden bericht.</string> + <string name="title_home">Start</string> + <string name="title_notifications">Meldingen</string> + <string name="title_public_local">Lokaal</string> + <string name="title_public_federated">Globaal</string> + <string name="title_direct_messages">Directe berichten</string> + <string name="title_tab_preferences">Tabs</string> + <string name="title_view_thread">Gesprek</string> + <string name="title_posts">Berichten</string> + <string name="title_posts_with_replies">Met reacties</string> + <string name="title_posts_pinned">Vastgezet</string> + <string name="title_follows">Volgend</string> + <string name="title_followers">Volgers</string> + <string name="title_favourites">Favorieten</string> + <string name="title_mutes">Genegeerde gebruikers</string> + <string name="title_blocks">Geblokkeerde gebruikers</string> + <string name="title_follow_requests">Volgverzoeken</string> + <string name="title_edit_profile">Profiel bewerken</string> + <string name="title_drafts">Concepten</string> + <string name="title_licenses">Licenties</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s boostte</string> + <string name="post_sensitive_media_title">Gevoelige inhoud</string> + <string name="post_media_hidden_title">Verborgen media</string> + <string name="post_sensitive_media_directions">Klik om te bekijken</string> + <string name="post_content_warning_show_more">Meer tonen</string> + <string name="post_content_warning_show_less">Minder tonen</string> + <string name="post_content_show_more">Uitklappen</string> + <string name="post_content_show_less">Inklappen</string> + <string name="message_empty">Hier is niets.</string> + <string name="footer_empty">Niets te zien. Swipe naar beneden om te verversen!</string> + <string name="notification_reblog_format">%1$s boostte jouw bericht</string> + <string name="notification_favourite_format">%1$s markeerde jouw bericht als favoriet</string> + <string name="notification_follow_format">%1$s volgt jou</string> + <string name="report_username_format">Rapporteer @%1$s</string> + <string name="report_comment_hint">Extra opmerkingen\?</string> + <string name="action_quick_reply">Snelle reactie</string> + <string name="action_reply">Reageren</string> + <string name="action_reblog">Boosten</string> + <string name="action_unreblog">Boost verwijderen</string> + <string name="action_favourite">Als favoriet markeren</string> + <string name="action_unfavourite">Favoriet verwijderen</string> + <string name="action_more">Meer</string> + <string name="action_compose">Bericht schrijven</string> + <string name="action_login">Aanmelden met Tusky</string> + <string name="action_logout">Uitloggen</string> + <string name="action_logout_confirm">Weet je zeker dat je %1$s wil afmelden\? Dit verwijdert alle lokale gegevens van het account, inclusief conceptberichten en voorkeuren.</string> + <string name="action_follow">Volgen</string> + <string name="action_unfollow">Ontvolgen</string> + <string name="action_block">Blokkeren</string> + <string name="action_unblock">Deblokkeren</string> + <string name="action_hide_reblogs">Boosts verbergen</string> + <string name="action_show_reblogs">Boosts tonen</string> + <string name="action_report">Rapporteren</string> + <string name="action_delete">Verwijderen</string> + <string name="action_send">Toot!</string> + <string name="action_send_public">Toot!</string> + <string name="action_retry">Opnieuw proberen</string> + <string name="action_close">Sluiten</string> + <string name="action_view_profile">Profiel</string> + <string name="action_view_preferences">App-voorkeuren</string> + <string name="action_view_account_preferences">Accountvoorkeuren</string> + <string name="action_view_favourites">Favorieten</string> + <string name="action_view_mutes">Genegeerde gebruikers</string> + <string name="action_view_blocks">Geblokkeerde gebruikers</string> + <string name="action_view_follow_requests">Volgverzoeken</string> + <string name="action_view_media">Media</string> + <string name="action_open_in_web">Open in webbrowser</string> + <string name="action_add_media">Media toevoegen</string> + <string name="action_photo_take">Foto maken</string> + <string name="action_share">Delen</string> + <string name="action_mute">Negeren</string> + <string name="action_unmute">Niet langer negeren</string> + <string name="action_mention">Vermelden</string> + <string name="action_hide_media">Media verbergen</string> + <string name="action_open_drawer">Menu openen</string> + <string name="action_save">Opslaan</string> + <string name="action_edit_profile">Profiel bewerken</string> + <string name="action_edit_own_profile">Bewerken</string> + <string name="action_undo">Ongedaan maken</string> + <string name="action_accept">Goedkeuren</string> + <string name="action_reject">Afwijzen</string> + <string name="action_search">Zoeken</string> + <string name="action_access_drafts">Concepten</string> + <string name="action_toggle_visibility">Zichtbaarheid bericht</string> + <string name="action_content_warning">Tekstwaarschuwing</string> + <string name="action_emoji_keyboard">Emojis</string> + <string name="action_add_tab">Tab toevoegen</string> + <string name="action_links">Links</string> + <string name="action_mentions">Vermeldingen</string> + <string name="action_hashtags">Hashtags</string> + <string name="action_open_reblogger">Auteur van deze boost openen</string> + <string name="action_open_reblogged_by">Boosts tonen</string> + <string name="action_open_faved_by">Favorieten tonen</string> + <string name="title_hashtags_dialog">Hashtags</string> + <string name="title_mentions_dialog">Vermeldingen</string> + <string name="title_links_dialog">Links</string> + <string name="action_open_media_n">Media #%1$d openen</string> + <string name="download_image">%1$s aan het downloaden</string> + <string name="action_copy_link">Link kopiëren</string> + <string name="action_open_as">Als %1$s openen</string> + <string name="action_share_as">Delen als …</string> + <string name="send_post_link_to">Link van het bericht delen met…</string> + <string name="send_post_content_to">Inhoud van het bericht delen met…</string> + <string name="send_media_to">Media delen met …</string> + <string name="confirmation_reported">Verzonden!</string> + <string name="confirmation_unblocked">Gebruiker is gedeblokkeerd</string> + <string name="confirmation_unmuted">Gebruiker wordt niet langer genegeerd</string> + <string name="hint_domain">Welke Mastodonserver?</string> + <string name="hint_compose">Wat wil je kwijt?</string> + <string name="hint_content_warning">Tekstwaarschuwing</string> + <string name="hint_display_name">Weergavenaam</string> + <string name="hint_note">Bio</string> + <string name="hint_search">Zoeken</string> + <string name="search_no_results">Geen resultaten</string> + <string name="label_quick_reply">Reageren…</string> + <string name="label_avatar">Avatar</string> + <string name="label_header">Omslagfoto</string> + <string name="link_whats_an_instance">Wat is een Mastodonserver?</string> + <string name="login_connection">Aan het verbinden…</string> + <string name="dialog_whats_an_instance">Het adres of domein van elke Mastodonserver kan hier worden ingevoerd, zoals mastodon.social, mastodon.nl, octodon.social en <a href="https://instances.social">nog veel meer!</a> +\n +\nWanneer je nog geen account hebt, kun je de naam van de Mastodonserver waar jij je graag wil registeren invoeren, waarna je daar een account kunt aanmaken. +\n +\nEen Mastodonserver (Engels: instance) is een computerserver waar jouw account zich bevindt (vergelijk het met een e-mailserver). Je kan eenvoudig mensen van andere servers volgen en met ze communiceren, alsof jullie met elkaar op dezelfde website zitten. +\n +\n Meer informatie kun je vinden op <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="dialog_title_finishing_media_upload">Uploaden media bijna voltooid</string> + <string name="dialog_message_uploading_media">Aan het uploaden…</string> + <string name="dialog_download_image">Downloaden</string> + <string name="dialog_message_cancel_follow_request">Het volgverzoek intrekken?</string> + <string name="dialog_unfollow_warning">Dit account ontvolgen?</string> + <string name="dialog_delete_post_warning">Dit bericht verwijderen\?</string> + <string name="visibility_public">Openbaar: op openbare tijdlijnen tonen</string> + <string name="visibility_unlisted">Minder openbaar: niet op openbare tijdlijnen tonen</string> + <string name="visibility_private">Alleen volgers: alleen aan jouw volgers tonen</string> + <string name="visibility_direct">Direct: alleen aan vermelde gebruikers tonen</string> + <string name="pref_title_edit_notification_settings">Meldingen bewerken</string> + <string name="pref_title_notifications_enabled">Meldingen</string> + <string name="pref_title_notification_alerts">Waarschuwen met</string> + <string name="pref_title_notification_alert_sound">geluid</string> + <string name="pref_title_notification_alert_vibrate">trillen</string> + <string name="pref_title_notification_alert_light">licht</string> + <string name="pref_title_notification_filters">Waarschuw mij wanneer</string> + <string name="pref_title_notification_filter_mentions">ik word vermeld</string> + <string name="pref_title_notification_filter_follows">ik word gevolgd</string> + <string name="pref_title_notification_filter_reblogs">mijn berichten werden geboost</string> + <string name="pref_title_notification_filter_favourites">mijn berichten zijn als favoriet gemarkeerd</string> + <string name="pref_title_appearance_settings">Uiterlijk</string> + <string name="pref_title_app_theme">Thema</string> + <string name="pref_title_timelines">Tijdlijnen</string> + <string name="app_them_dark">Donker</string> + <string name="app_theme_light">Licht</string> + <string name="app_theme_black">Zwart</string> + <string name="app_theme_auto">Automatisch tijdens zonsop- en ondergang</string> + <string name="app_theme_system">Systeemthema gebruiken</string> + <string name="pref_title_browser_settings">Webbrowser</string> + <string name="pref_title_custom_tabs">Aangepaste tabbladen gebruiken</string> + <string name="pref_title_language">Taal</string> + <string name="pref_title_post_filter">Filteren</string> + <string name="pref_title_post_tabs">Tijdlijnen</string> + <string name="pref_title_show_boosts">Boosts tonen</string> + <string name="pref_title_show_replies">Reacties tonen</string> + <string name="pref_title_show_media_preview">Voorbeelden van media tonen</string> + <string name="pref_title_proxy_settings">Proxy</string> + <string name="pref_title_http_proxy_settings">HTTP-proxy</string> + <string name="pref_title_http_proxy_enable">HTTP-proxy inschakelen</string> + <string name="pref_title_http_proxy_server">Serveradres van HTTP-proxy</string> + <string name="pref_title_http_proxy_port">Poort van HTTP-proxy</string> + <string name="pref_default_post_privacy">Standaardzichtbaarheid van jouw berichten</string> + <string name="pref_default_media_sensitivity">Media altijd als gevoelig markeren</string> + <string name="pref_publishing">Publiceren</string> + <string name="pref_failed_to_sync">Synchroniseren</string> + <string name="post_privacy_public">Openbaar</string> + <string name="post_privacy_unlisted">Minder openbaar</string> + <string name="post_privacy_followers_only">Alleen volgers</string> + <string name="pref_post_text_size">Tekstgrootte van berichten</string> + <string name="post_text_size_smallest">Kleinst</string> + <string name="post_text_size_small">Klein</string> + <string name="post_text_size_medium">Standaard</string> + <string name="post_text_size_large">Groot</string> + <string name="post_text_size_largest">Grootst</string> + <string name="notification_mention_name">Nieuwe vermeldingen</string> + <string name="notification_mention_descriptions">Meldingen over nieuwe vermeldingen</string> + <string name="notification_follow_name">Nieuwe volgers</string> + <string name="notification_follow_description">Meldingen over nieuwe volgers</string> + <string name="notification_boost_name">Boosts</string> + <string name="notification_boost_description">Meldingen wanneer jouw berichten worden geboost</string> + <string name="notification_favourite_name">Favorieten</string> + <string name="notification_favourite_description">Meldingen wanneer jouw berichten als favoriet worden gemarkeerd</string> + <string name="notification_mention_format">%1$s vermeldde jou</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s en %4$d anderen</string> + <string name="notification_summary_medium">%1$s, %2$s en %3$s</string> + <string name="notification_summary_small">%1$s en %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d nieuwe interactie</item> + <item quantity="other">%1$d nieuwe interacties</item> + </plurals> + <string name="description_account_locked">Besloten account</string> + <string name="about_title_activity">Over</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tusky is opensource- en vrije software. De licentie valt onder de GNU Algemene Publieke Licentie versie 3. Je kunt de licentie hier bekijken: https://www.gnu.org/licenses/gpl-3.0.nl.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">Projectwebsite: +\nhttps://tusky.app</string> + <string name="about_bug_feature_request_site">Foutmeldingen & nieuwe functies aanvragen: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Tusky\'s profiel</string> + <string name="post_share_content">Inhoud van bericht delen</string> + <string name="post_share_link">Link van het bericht delen</string> + <string name="post_media_images">Afbeeldingen</string> + <string name="post_media_video">Video</string> + <string name="state_follow_requested">Volgverzoek verzonden</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">over %1$dj</string> + <string name="abbreviated_in_days">over %1$dd</string> + <string name="abbreviated_in_hours">over %1$du</string> + <string name="abbreviated_in_minutes">over %1$dm</string> + <string name="abbreviated_in_seconds">over %1$ds</string> + <string name="abbreviated_years_ago">%1$dj</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_hours_ago">%1$du</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="follows_you">Volgt jou</string> + <string name="pref_title_alway_show_sensitive_media">Altijd gevoelige inhoud (nsfw) tonen</string> + <string name="title_media">Media</string> + <string name="replying_to">Aan het reageren op @%1$s</string> + <string name="load_more_placeholder_text">meer laden</string> + <string name="add_account_name">Account toevoegen</string> + <string name="add_account_description">Een nieuw Mastodonaccount toevoegen</string> + <string name="action_lists">Lijsten</string> + <string name="title_lists">Lijsten</string> + <string name="compose_active_account_description">Berichten plaatsen als %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Omschrijf inhoud voor iemand met een visuele beperking (tekenlimiet is %1$d)</item> + <item quantity="other">Omschrijf inhoud voor mensen met een visuele beperking (tekenlimiet is %1$d)</item> + </plurals> + <string name="action_set_caption">Beschrijving toevoegen</string> + <string name="action_remove">Verwijderen</string> + <string name="lock_account_label">Account besloten maken</string> + <string name="lock_account_label_description">Handmatige goedkeuring vereist voor volgers</string> + <string name="compose_save_draft">Concept bewaren?</string> + <string name="send_post_notification_title">Bericht wordt verzonden…</string> + <string name="send_post_notification_error_title">Verzenden van het bericht is mislukt</string> + <string name="send_post_notification_channel_name">Berichten worden verzonden</string> + <string name="send_post_notification_cancel_title">Verzenden geannuleerd</string> + <string name="send_post_notification_saved_content">Een kopie van het bericht werd als concept opgeslagen</string> + <string name="action_compose_shortcut">Bericht schrijven</string> + <string name="error_no_custom_emojis">Jouw server %1$s heeft geen lokale emojis</string> + <string name="emoji_style">Emojistijl</string> + <string name="system_default">Systeemstandaard</string> + <string name="download_fonts">Je moet eerst deze emoji-sets downloaden</string> + <string name="performing_lookup_title">Aan het zoeken…</string> + <string name="expand_collapse_all_posts">Alle berichten in- of uitklappen</string> + <string name="action_open_post">Bericht openen</string> + <string name="restart_required">Herstarten app vereist</string> + <string name="restart_emoji">Je moet Tusky herstarten om deze veranderingen te kunnen doorvoeren</string> + <string name="later">Later</string> + <string name="restart">Herstarten</string> + <string name="caption_systememoji">Standaard emojiset van jouw apparaat</string> + <string name="caption_blobmoji">De Blob-emojis van Android 4.4–7.1</string> + <string name="caption_twemoji">Standaard emojiset van Mastodon</string> + <string name="download_failed">Downloaden mislukt</string> + <string name="profile_badge_bot_text">Bot</string> + <string name="account_moved_description">%1$s is verhuisd naar:</string> + <string name="reblog_private">Boost naar oorspronkelijke ontvangers</string> + <string name="unreblog_private">Niet langer boosten</string> + <string name="license_description">Tusky bevat broncode en onderdelen van de volgende opensourceprojecten:</string> + <string name="license_apache_2">Gelicenseerd onder de Apache-licentie (kopie hieronder)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Metadata profiel</string> + <string name="profile_metadata_add">Metadata toevoegen</string> + <string name="profile_metadata_label_label">Label</string> + <string name="profile_metadata_content_label">Inhoud</string> + <string name="pref_title_absolute_time">Absolute tijd gebruiken</string> + <string name="label_remote_account">De informatie hieronder kan een incompleet beeld geven van dit gebruikersprofiel. Druk hier om het volledige profiel in de webbrowser te openen.</string> + <string name="unpin_action">Losmaken</string> + <string name="pin_action">Vastmaken</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> favoriet</item> + <item quantity="other"><b>%1$s</b> favorieten</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> boost</item> + <item quantity="other"><b>%1$s</b> boosts</item> + </plurals> + <string name="title_reblogged_by">Geboost door</string> + <string name="title_favourited_by">Als favoriet gemarkeerd door</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s en %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s en %3$d meer</string> + <string name="description_post_media">Media: %1$s</string> + <string name="description_post_cw">Inhoudswaarschuwing: %1$s</string> + <string name="description_post_media_no_description_placeholder">Geen omschrijving</string> + <string name="description_post_reblogged">Geboost</string> + <string name="description_post_favourited">Als favoriet gemarkeerd</string> + <string name="description_visibility_public">Openbaar</string> + <string name="description_visibility_unlisted">Minder openbaar</string> + <string name="description_visibility_private">Volgers</string> + <string name="description_visibility_direct">Direct</string> + <string name="download_media">Media downloaden</string> + <string name="downloading_media">Media aan het downloaden</string> + <string name="pref_title_timeline_filters">Filters</string> + <string name="pref_title_public_filter_keywords">Openbare tijdlijnen</string> + <string name="pref_title_thread_filter_keywords">Gesprekken</string> + <string name="filter_addition_title">Filter toevoegen</string> + <string name="filter_edit_title">Filter bewerken</string> + <string name="filter_dialog_remove_button">Verwijderen</string> + <string name="filter_dialog_update_button">Bijwerken</string> + <string name="filter_add_description">Zinsdeel om te filteren</string> + <string name="error_create_list">Kon geen lijst aanmaken</string> + <string name="error_rename_list">Kan lijst niet updaten</string> + <string name="error_delete_list">Kon de lijst niet verwijderen</string> + <string name="action_create_list">Lijst aanmaken</string> + <string name="action_rename_list">Lijst updaten</string> + <string name="action_delete_list">Lijst verwijderen</string> + <string name="hint_search_people_list">Naar mensen zoeken die je volgt</string> + <string name="action_add_to_list">Account aan de lijst toevoegen</string> + <string name="action_remove_from_list">Account uit de lijst verwijderen</string> + <string name="hint_list_name">Naam van lijst</string> + <string name="edit_hashtag_hint">Hashtag zonder #</string> + <string name="action_delete_and_redraft">Verwijderen en herschrijven</string> + <string name="dialog_redraft_post_warning">Dit bericht verwijderen en herschrijven\?</string> + <string name="notifications_clear">Verwijder</string> + <string name="notifications_apply_filter">Filter</string> + <string name="filter_apply">Toepassen</string> + <string name="compose_shortcut_long_label">Bericht schrijven</string> + <string name="compose_shortcut_short_label">Schrijven</string> + <string name="pref_title_bot_overlay">Toon indicator voor bots</string> + <string name="notification_clear_text">Weet je zeker dat je alle meldingen permanent wilt verwijderen\?</string> + <string name="poll_info_format"> <!-- 15 votes • 1 hour left --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s stem</item> + <item quantity="other">%1$s stemmen</item> + </plurals> + <string name="poll_info_time_absolute">eindigt op %1$s</string> + <string name="poll_info_closed">gesloten</string> + <string name="poll_vote">Stemmen</string> + <string name="pref_title_notification_filter_poll">er zijn polls zijn beëindigd</string> + <string name="notification_poll_name">Polls</string> + <string name="notification_poll_description">Meldingen over polls die zijn beëindigd</string> + <string name="poll_ended_voted">Een poll waaraan jij hebt meegedaan is beëindigd</string> + <string name="poll_ended_created">Een poll die jij hebt aangemaakt is beëindigd</string> + <string name="title_domain_mutes">Verborgen domeinen</string> + <string name="action_view_domain_mutes">Verborgen domeinen</string> + <string name="action_mute_domain">%1$s negeren</string> + <string name="confirmation_domain_unmuted">%1$s niet langer verborgen</string> + <string name="mute_domain_warning_dialog_ok">Volledig domein verbergen</string> + <string name="pref_title_animate_gif_avatars">GIF-avatars animeren</string> + <string name="caption_notoemoji">Google\'s huidige emojiset</string> + <string name="mute_domain_warning">Weet je zeker dat je alles van %1$s wilt blokkeren\? Je zult op alle openbare tijdlijnen en in jouw meldingen geen inhoud van dat domein zien. Jouw volgers van dat domein worden verwijderd.</string> + <string name="description_poll">Poll met keuzes: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="compose_preview_image_description">Acties voor afbeelding %1$s</string> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d dag te gaan</item> + <item quantity="other">%1$d dagen te gaan</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d uur te gaan</item> + <item quantity="other">%1$d uur te gaan</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d minuut te gaan</item> + <item quantity="other">%1$d minuten te gaan</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d seconde over</item> + <item quantity="other">%1$d seconden over</item> + </plurals> + <string name="button_continue">Doorgaan</string> + <string name="button_back">Terug</string> + <string name="button_done">Klaar</string> + <string name="report_sent_success">Het rapporteren van @%1$s is geslaagd</string> + <string name="hint_additional_info">Extra opmerkingen</string> + <string name="report_remote_instance">Verder naar %1$s</string> + <string name="failed_report">Het rapporteren is mislukt</string> + <string name="failed_fetch_posts">Het ophalen van berichten is mislukt</string> + <string name="report_description_1">Deze rapportage wordt naar jouw servermoderator(en) gestuurd. Je kunt hieronder een uitleg geven over waarom je het account wilt rapporteren:</string> + <string name="report_description_remote_instance">Het account is van een andere server. Wil je ook een geanonimiseerde kopie van de rapportage daarnaartoe sturen\?</string> + <string name="filter_dialog_whole_word">Heel woord</string> + <string name="filter_dialog_whole_word_description">Wanneer het trefwoord of zinsdeel alfanumeriek is, wordt het alleen gefilterd wanneer het hele woord overeenkomt</string> + <string name="duration_5_min">5 minuten</string> + <string name="duration_30_min">30 minuten</string> + <string name="duration_1_hour">1 uur</string> + <string name="duration_6_hours">6 uur</string> + <string name="duration_1_day">1 dag</string> + <string name="duration_3_days">3 dagen</string> + <string name="duration_7_days">7 dagen</string> + <string name="add_poll_choice">Voeg keuze toe</string> + <string name="poll_allow_multiple_choices">Meerdere keuzes</string> + <string name="poll_new_choice_hint">Keuze %1$d</string> + <string name="edit_poll">Bewerken</string> + <string name="title_bookmarks">Bladwijzers</string> + <string name="title_scheduled_posts">Ingeplande berichten</string> + <string name="action_bookmark">Bladwijzer</string> + <string name="action_edit">Bewerken</string> + <string name="action_view_bookmarks">Bladwijzers</string> + <string name="action_add_poll">Poll toevoegen</string> + <string name="action_access_scheduled_posts">Ingeplande berichten</string> + <string name="action_schedule_post">Ingepland bericht</string> + <string name="action_reset_schedule">Herstellen</string> + <string name="about_powered_by_tusky">Mogelijk gemaakt door Tusky</string> + <string name="pref_title_alway_open_spoiler">Berichten met tekstwaarschuwingen altijd uitklappen</string> + <string name="description_post_bookmarked">Als bladwijzer toegevoegd</string> + <string name="select_list_title">Kies een lijst</string> + <string name="list">Lijst</string> + <string name="title_accounts">Accounts</string> + <string name="failed_search">Zoeken mislukt</string> + <string name="create_poll_title">Poll</string> + <string name="post_lookup_error_format">Fout tijdens het opzoeken van bericht %1$s</string> + <string name="no_drafts">Je hebt nog geen concepten.</string> + <string name="no_scheduled_posts">Je hebt nog geen ingeplande berichten.</string> + <string name="warning_scheduling_interval">Om in te plannen moet je in Mastodon een minimum interval van 5 minuten gebruiken.</string> + <string name="notification_follow_request_name">Volgverzoeken</string> + <string name="hashtags">Hashtags</string> + <string name="post_media_attachments">Bijlagen</string> + <string name="pref_title_notification_filter_follow_requests">volgverzoek verstuurd</string> + <string name="action_unsubscribe_account">Afmelden</string> + <string name="action_subscribe_account">Abonneren</string> + <string name="drafts_post_reply_removed">Het bericht waarvoor jij een reactie had opgesteld, is verwijderd</string> + <string name="drafts_post_failed_to_send">Het versturen van dit bericht is mislukt!</string> + <string name="dialog_delete_list_warning">Weet je zeker dat je de lijst %1$s wilt verwijderen\?</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Je kan niet meer dan %1$d mediabijlage uploaden.</item> + <item quantity="other">Je kan niet meer dan %1$d mediabijlagen uploaden.</item> + </plurals> + <string name="limit_notifications">Meldingen op tijdlijn beperken</string> + <string name="account_note_saved">Opgeslagen!</string> + <string name="account_note_hint">Jouw eigen opmerking over dit account</string> + <string name="pref_title_wellbeing_mode">Welzijn</string> + <string name="pref_title_hide_top_toolbar">De bovenste werkbalk verbergen</string> + <string name="pref_title_confirm_reblogs">Vraag bevestiging voor het boosten</string> + <string name="pref_title_show_cards_in_timelines">Linkpreviews in tijdlijnen weergeven</string> + <string name="no_announcements">Er zijn geen aankondigingen.</string> + <string name="duration_indefinite">Oneindig</string> + <string name="label_duration">Looptijd</string> + <string name="pref_title_enable_swipe_for_tabs">Swipebewegingen om tussen tabs te schakelen inschakelen</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s persoon</item> + <item quantity="other">%1$s personen</item> + </plurals> + <string name="add_hashtag_title">Hashtag toevoegen</string> + <string name="post_media_audio">Geluid</string> + <string name="notification_subscription_description">Meldingen wanneer iemand waar je op bent geabonneerd een nieuw bericht plaatst</string> + <string name="notification_subscription_name">Nieuwe berichten</string> + <string name="notification_follow_request_description">Meldingen over volgverzoeken</string> + <string name="pref_main_nav_position_option_bottom">Onder</string> + <string name="pref_main_nav_position_option_top">Boven</string> + <string name="pref_title_animate_custom_emojis">Lokale emojis animeren</string> + <string name="pref_title_gradient_for_media">Kleurverloop weergeven voor verborgen media</string> + <string name="pref_title_notification_filter_subscriptions">iemand waar ik op ben geabonneerd heeft een nieuw bericht geplaatst</string> + <string name="dialog_mute_hide_notifications">Meldingen verbergen</string> + <string name="dialog_mute_warning">\@%1$s negeren\?</string> + <string name="dialog_block_warning">\@%1$s blokkeren\?</string> + <string name="action_unmute_conversation">Gesprek niet meer negeren</string> + <string name="action_mute_conversation">Gesprek negeren</string> + <string name="action_unmute_domain">%1$s niet meer negeren</string> + <string name="action_unmute_desc">%1$s niet meer negeren</string> + <string name="notification_subscription_format">%1$s heeft zojuist een bericht geplaatst</string> + <string name="notification_follow_request_format">%1$s verzoekt u te volgen</string> + <string name="title_announcements">Aankondigingen</string> + <string name="review_notifications">Meldingen beoordelen</string> + <string name="draft_deleted">Concept verwijderd</string> + <string name="wellbeing_hide_stats_posts">Kwantitatieve statistieken voor berichten verbergen</string> + <string name="drafts_failed_loading_reply">Laden van reactie-informatie mislukt</string> + <string name="wellbeing_hide_stats_profile">Kwantitatieve statistieken in profielen verbergen</string> + <string name="pref_main_nav_position">Hoofd navigatiepositie</string> + <string name="dialog_delete_conversation_warning">Dit gesprek verwijderen\?</string> + <string name="action_delete_conversation">Gesprek verwijderen</string> + <string name="follow_requests_info">Ook al heb je geen besloten account, de medewerkers van %1$s dachten dat je misschien de volgverzoeken van deze accounts handmatig zou willen controleren.</string> + <string name="wellbeing_mode_notice">Bepaalde informatie die invloed kan hebben op jouw geestelijk welzijn zal worden verborgen. Dit bevat onder andere: +\n +\n- Meldingen over favorieten, boosts en volgers +\n- Weergave van het aantal favorieten en boosts per bericht +\n- Statistieken over het aantal volgers en berichten op profielen +\n +\nPushmeldingen worden hierdoor niet beïnvloed, maar je kunt de voorkeuren voor meldingen handmatig wijzigen.</string> + <string name="notification_sign_up_format">%1$s heeft zich geregistreerd</string> + <string name="tips_push_notification_migration">Alle accounts opnieuw inloggen i.v.m. ondersteuning pushmeldingen.</string> + <string name="error_multimedia_size_limit">Afbeeldingen en video\'s kunnen niet groter zijn dan %1$s MB.</string> + <string name="error_image_edit_failed">Deze afbeelding kon niet worden bewerkt.</string> + <string name="title_login">Inloggen</string> + <string name="title_migration_relogin">Opnieuw inloggen i.v.m. pushmeldingen</string> + <string name="notification_update_format">%1$s heeft diens bericht bewerkt</string> + <string name="action_unbookmark">Bladwijzer verwijderen</string> + <string name="action_dismiss">Afwijzen</string> + <string name="action_details">Details</string> + <string name="pref_title_notification_filter_sign_ups">iemand heeft zich geregistreerd</string> + <string name="pref_title_notification_filter_updates">een bericht waarmee ik interactie had is bewerkt</string> + <string name="notification_sign_up_name">Registraties</string> + <string name="notification_sign_up_description">Meldingen over nieuwe gebruikers</string> + <string name="notification_update_name">Bewerkingen van berichten</string> + <string name="notification_update_description">Meldingen wanneer berichten waarmee je interactie had werden bewerkt</string> + <string name="status_count_one_plus">1+</string> + <string name="action_edit_image">Afbeelding bewerken</string> + <string name="duration_30_days">30 dagen</string> + <string name="duration_60_days">60 dagen</string> + <string name="duration_14_days">14 dagen</string> + <string name="duration_90_days">90 dagen</string> + <string name="duration_180_days">180 dagen</string> + <string name="duration_365_days">365 dagen</string> + <string name="pref_title_confirm_favourites">Vraag bevestiging voor het markeren als favoriet</string> + <string name="tusky_compose_post_quicksetting_label">Bericht schrijven</string> + <string name="account_date_joined">Geregistreerd in %1$s</string> + <string name="saving_draft">Concept wordt opgeslagen…</string> + <string name="error_loading_account_details">Laden van accountdetails mislukt</string> + <string name="error_could_not_load_login_page">De inlogpagina kon niet worden geladen.</string> + <string name="description_post_language">Taal van jouw berichten</string> + <string name="duration_no_change">(geen verandering)</string> + <string name="pref_title_show_self_username">Gebruikersnaam op werkbalken tonen</string> + <string name="set_focus_description">Tik of sleep de cirkel naar een centraal focuspunt dat op elke thumbnail zichtbaar moet blijven.</string> + <string name="dialog_push_notification_migration">Om pushmeldingen via UnifiedPush te kunnen gebruiken, moet Tusky zich op meldingen van jouw Mastodon-server abonneren. Dit betekent dat je opnieuw moet inloggen om de OAuth-toestemmingen voor Tusky te wijzigen. Het hier of onder accountvoorkeuren opnieuw inloggen behoudt jouw lokale concepten en buffer.</string> + <string name="dialog_push_notification_migration_other_accounts">Je hebt opnieuw op jouw huidige account ingelogd om toestemming voor pushmeldingen aan Tusky te verlenen. Je hebt echter nog andere accounts die nog niet op deze manier zijn overgezet. Ga naar deze accounts en log één voor één opnieuw in om UnifiedPush-meldingen ook daar in te schakelen.</string> + <string name="pref_show_self_username_always">Altijd</string> + <string name="pref_show_self_username_disambiguate">Wanneer meerdere accounts zijn ingelogd</string> + <string name="pref_show_self_username_never">Nooit</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="action_set_focus">Focuspunt instellen</string> + <string name="error_following_hashtag_format">Fout tijdens het volgen van #%1$s</string> + <string name="error_unfollowing_hashtag_format">Fout tijdens het ontvolgen van #%1$s</string> + <string name="delete_scheduled_post_warning">Dit ingeplande bericht verwijderen\?</string> + <string name="pref_title_reading_order">Leesvolgorde</string> + <string name="pref_reading_order_oldest_first">Oudste eerst</string> + <string name="pref_reading_order_newest_first">Nieuwste eerst</string> + <string name="failed_to_add_to_list">Account toevoegen aan de lijst is mislukt</string> + <string name="action_add_or_remove_from_list">Toevoegen of verwijderen van lijst</string> + <string name="failed_to_pin">Kan niet vastmaken</string> + <string name="pref_summary_http_proxy_disabled">Uitgeschakeld</string> + <string name="failed_to_remove_from_list">Account verwijderen van de lijst is mislukt</string> + <string name="compose_unsaved_changes">Er zijn niet opgeslagen wijzigingen.</string> + <string name="mute_notifications_switch">Meldingen negeren</string> + <string name="title_edits">Bewerkingen</string> + <string name="pref_default_post_language">Standaardtaal van berichten</string> + <string name="notification_report_name">Rapporten</string> + <string name="description_post_edited">Bewerkt</string> + <string name="status_edit_info">%1$s bewerkte</string> + <string name="status_created_info">%1$s maakte</string> + <string name="instance_rule_info">Door in te loggen ben je het eens met de regels van %1$s.</string> + <string name="report_category_spam">Spam</string> + <string name="report_category_other">Overig</string> + <string name="hint_media_description_missing">Media moet een beschrijving hebben.</string> + <string name="failed_to_unpin">Kan niet losmaken</string> + <string name="instance_rule_title">%1$s regels</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="report_category_violation">Regelovertreding</string> + <string name="no_lists">Je hebt geen lijsten.</string> + <string name="status_created_at_now">nu</string> + <string name="post_media_alt">ALT</string> + <string name="action_unfollow_hashtag_format">Ontvolg #%1$s\?</string> + <string name="error_muting_hashtag_format">Fout bij negeren #%1$s</string> + <string name="action_continue_edit">Ga door met bewerken</string> + <string name="pref_title_notification_filter_reports">Er is een nieuw rapport</string> + <string name="notification_report_format">Nieuw rapport over %1$s</string> + <string name="account_username_copied">Gebruikersnaam gekopieerd</string> + <string name="confirmation_hashtag_unfollowed">#%1$s ontvolgd</string> + <string name="title_followed_hashtags">Gevolgde hashtags</string> + <string name="dialog_follow_hashtag_title">Volg hashtag</string> + <string name="dialog_follow_hashtag_hint">#hashtag</string> + <string name="action_post_failed">Upload mislukt</string> + <string name="action_post_failed_detail">Je bericht kan niet worden geüpload en is opgeslagen in concepten. +\n +\nEr kon geen contact worden opgenomen met de server, of het bericht werd geweigerd.</string> + <string name="action_post_failed_detail_plural">Je berichten kunnen niet worden geüpload en zijn opgeslagen in concepten. +\n +\nEr kon geen contact worden opgenomen met de server of de berichten werden geweigerd.</string> + <string name="action_post_failed_do_nothing">Afwijzen</string> + <string name="action_post_failed_show_drafts">Concepten tonen</string> + <string name="error_unmuting_hashtag_format">Fout bij unmuting #%1$s</string> + <string name="action_browser_login">Inloggen met een browser</string> + <string name="action_add_reaction">reactie toevoegen</string> + <string name="action_discard">Veranderingen ongedaan maken</string> + <string name="post_edited">Bewerkt %1$s</string> + <string name="notification_header_report_format">%1$s gerapporteerd %2$s</string> + <string name="notification_summary_report_format">%1$s · %2$d berichten bijgevoegd</string> + <string name="action_share_account_link">Link naar account delen</string> + <string name="action_share_account_username">Deel de gebruikersnaam van het account</string> + <string name="send_account_username_to">Accountgebruikersnaam delen om…</string> + <string name="send_account_link_to">Account-URL delen met…</string> + <string name="error_following_hashtags_unsupported">Deze instance ondersteunt niet het volgen van hashtags.</string> + <string name="error_status_source_load">Fout bij het laden van de statusbron van server.</string> + <string name="title_public_trending_hashtags">Trending hashtags</string> + <string name="pref_summary_http_proxy_missing"><niet ingesteld></string> + <string name="pref_summary_http_proxy_invalid"><ongeldig></string> + <string name="action_refresh">Ververs</string> + <string name="post_media_image">Afbeelding</string> + <string name="ui_error_reblog">Boosten bericht mislukt: %1$s</string> + <string name="ui_error_vote">Stemmen in peiling mislukt: %1$s</string> + <string name="pref_title_http_proxy_port_message">Poort moet liggen tussen %1$d en %2$d</string> + <string name="notification_unknown_name">Onbekend</string> + <string name="ui_error_clear_notifications">Wissen meldingen mislukt: %1$s</string> + <string name="ui_success_accepted_follow_request">Volgverzoek geaccepteerd</string> + <string name="select_list_manage">Lijsten beheren</string> + <string name="pref_title_account_filter_keywords">Profielen</string> + <string name="status_filtered_show_anyway">Toch tonen</string> + <string name="status_filter_placeholder_label_format">Gefilterd: %1$s</string> + <string name="hint_filter_title">Mijn filter</string> + <string name="label_filter_action">Filteractie</string> + <string name="filter_action_warn">Waarschuwen</string> + <string name="filter_action_hide">Verbergen</string> + <string name="filter_description_warn">Verberg met een waarschuwing</string> + <string name="filter_description_hide">Verberg helemaal</string> + <string name="action_add">Toevoegen</string> + <string name="filter_keyword_display_format">%1$s (heel woord)</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="accessibility_talking_about_tag">%1$d mensen bespreken hashtag %2$s</string> + <string name="total_usage">Totaal gebruik</string> + <string name="total_accounts">Totaal accounts</string> + <string name="ui_error_reject_follow_request">Afwijzen volgverzoek mislukt: %1$s</string> + <string name="pref_title_show_stat_inline">Toon bericht statistieken in tijdlijn</string> + <string name="ui_error_unknown">onbekende reden</string> + <string name="ui_error_accept_follow_request">Accepteren volgverzoek mislukt: %1$s</string> + <string name="label_filter_title">Titel</string> + <string name="load_newest_notifications">Laad nieuwste meldingen</string> + <string name="compose_delete_draft">Verwijder concept\?</string> + <string name="notification_notification_worker">Meldingen ophalen…</string> + <string name="notification_listenable_worker_name">Achtergrond activiteit</string> + <string name="notification_listenable_worker_description">Meldingen als Tusky werkt op de achtergrond</string> + <string name="about_device_info">%1$s %2$s +\nAndroid versie: %3$s +\nSDK versie: %4$d</string> + <string name="about_account_info_title">Je account</string> + <string name="about_account_info">\@%1$s@%2$s +\nVersie: %3$s</string> + <string name="about_copy">Kopieer versie en apparaatinformatie</string> + <string name="about_copied">Versie en apparaatinformatie gekopieerd</string> + <string name="error_media_upload_sending_fmt">De upload is mislukt: %1$s</string> + <string name="socket_timeout_exception">Contact zoeken met je server duurde te lang</string> + <string name="dialog_delete_filter_positive_action">Verwijder</string> + <string name="dialog_delete_filter_text">Verwijder filter \'%1$s\'\?</string> + <string name="pref_ui_text_size">UI tekstgrootte</string> + <string name="notification_prune_cache">Cache onderhoud…</string> + <string name="about_device_info_title">Je apparaat</string> + <string name="dialog_save_profile_changes_message">Wil je de wijzigingen aan je profiel bewaren\?</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-oc/strings.xml b/app/src/main/res/values-oc/strings.xml new file mode 100644 index 0000000..3f8743b --- /dev/null +++ b/app/src/main/res/values-oc/strings.xml @@ -0,0 +1,709 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">S\'es produch una error.</string> + <string name="error_empty">Aquò pot pas èsser void.</string> + <string name="error_invalid_domain">Lo domeni picat es pas valid</string> + <string name="error_failed_app_registration">L\'autenticacion en aquesta instància a fracassat. Se ten de se produire, ensajatz la connexion via Navigador via lo menú.</string> + <string name="error_no_web_browser_found">Cap de navegador web pas trobat d’utilizar.</string> + <string name="error_authorization_unknown">S\'es produch una error d\'autorizacion pas identificada. Se ten de se produire, ensajatz la connexion via Navigador via lo menú.</string> + <string name="error_authorization_denied">L\'autorizacion es estada regetada.</string> + <string name="error_retrieving_oauth_token">Fracàs de l’obtencion del geton d\'iniciacion de session.</string> + <string name="error_compose_character_limit">La publicacion es tròp longa !</string> + <string name="error_media_upload_type">Aqueste tip de fichièr se pòt pas mandar.</string> + <string name="error_media_upload_opening">Aqueste tip de fichièr se pòt pas dobrir.</string> + <string name="error_media_upload_permission">Cal permís de lectura del mèdia.</string> + <string name="error_media_download_permission">Cal pemís d\'escritura dins lo mèdia.</string> + <string name="error_media_upload_image_or_video">Se pòt pas ajustar imatges e una vidèo dins la meteissa publicacion.</string> + <string name="error_media_upload_sending">Fracàs en mandant.</string> + <string name="error_sender_account_gone">Error en enviant la publicacion.</string> + <string name="title_home">Acuèlh</string> + <string name="title_notifications">Notificacions</string> + <string name="title_public_federated">Federacion</string> + <string name="title_view_thread">Fil</string> + <string name="title_posts">Pòstes</string> + <string name="title_posts_with_replies">Amb responsas</string> + <string name="title_follows">Abonament</string> + <string name="title_followers">Seguidors</string> + <string name="title_favourites">Preferits</string> + <string name="title_mutes">Utilizaires silenciats</string> + <string name="title_blocks">Utilizaires blocats</string> + <string name="title_follow_requests">Demandas d’abonament</string> + <string name="title_edit_profile">Modificar lo perfil</string> + <string name="title_drafts">Borrolhons</string> + <string name="title_licenses">Licéncias</string> + <string name="post_boosted_format">%1$s a partejat</string> + <string name="post_sensitive_media_title">Contengut sensible</string> + <string name="post_media_hidden_title">Mèdia amagat</string> + <string name="post_sensitive_media_directions">Clicar per mostrar</string> + <string name="post_content_warning_show_more">Mostrar mai</string> + <string name="post_content_warning_show_less">Mostrar mens</string> + <string name="post_content_show_more">Desplegar</string> + <string name="post_content_show_less">Replegar</string> + <string name="footer_empty">I a pas res aquí. Davalatz per actualizar !</string> + <string name="notification_reblog_format">%1$s a partejat vòstra publicacion</string> + <string name="notification_favourite_format">%1$s a aimat vòstra publicacion</string> + <string name="notification_follow_format">%1$s vos sèc</string> + <string name="report_username_format">Denonciar @%1$s</string> + <string name="report_comment_hint">Cap comentari addicional ?</string> + <string name="action_quick_reply">Responsa rapida</string> + <string name="action_reply">Respondre</string> + <string name="action_reblog">Partejar</string> + <string name="action_favourite">Aimar</string> + <string name="action_more">Mai</string> + <string name="action_compose">Redactar</string> + <string name="action_login">Començar la session amb Tusky</string> + <string name="action_logout">Tampar la session</string> + <string name="action_logout_confirm">Volètz vertadièrament vos desconnectar del compte %1$s \? +\nAquò suprimirà totas las donadas localas del compte, tanben los borrolhons e las preferéncias.</string> + <string name="action_follow">Seguir</string> + <string name="action_unfollow">Quitar de seguir</string> + <string name="action_block">Blocar</string> + <string name="action_unblock">Quitar de blocar</string> + <string name="action_hide_reblogs">Rescondre los partatges</string> + <string name="action_show_reblogs">Mostrar los retuts</string> + <string name="action_report">Senhalar</string> + <string name="action_delete">Escafar</string> + <string name="action_send">TUT</string> + <string name="action_send_public">TUT !</string> + <string name="action_retry">Tornar ensajar</string> + <string name="action_close">Tampar</string> + <string name="action_view_profile">Perfil</string> + <string name="action_view_preferences">Preferéncias</string> + <string name="action_view_favourites">Preferits</string> + <string name="action_view_mutes">Utilizaires silenciats</string> + <string name="action_view_blocks">Utilizaires blocats</string> + <string name="action_view_follow_requests">Demandas d’abonament</string> + <string name="action_view_media">Mèdia</string> + <string name="action_open_in_web">Dobrir al navegador</string> + <string name="action_add_media">Ajustar un mèdia</string> + <string name="action_photo_take">Prendre una fotografia</string> + <string name="action_share">Partejar</string> + <string name="action_mute">Silenciar</string> + <string name="action_unmute">Quitar de silenciar</string> + <string name="action_mention">Mencionar</string> + <string name="action_hide_media">Amagar los mèdias</string> + <string name="action_open_drawer">Dobrir lo repertòri</string> + <string name="action_save">Enregistrar</string> + <string name="action_edit_profile">Modificar lo perfil</string> + <string name="action_edit_own_profile">Modificar</string> + <string name="action_undo">Anullar</string> + <string name="action_accept">Acceptar</string> + <string name="action_reject">Regetar</string> + <string name="action_search">Cercar</string> + <string name="action_access_drafts">Borrolhons</string> + <string name="action_toggle_visibility">Visibilitat de la publicacion</string> + <string name="action_content_warning">Avis de contengut</string> + <string name="action_emoji_keyboard">Clavièr Emoji</string> + <string name="download_image">Telecargament %1$s</string> + <string name="action_copy_link">Copiar lo ligam</string> + <string name="send_post_link_to">Partejar l\'URL del tut amb…</string> + <string name="send_post_content_to">Partejar vòstre tut amb…</string> + <string name="send_media_to">Partejar l’imatge amb…</string> + <string name="confirmation_reported">Enviat !</string> + <string name="confirmation_unblocked">Utilizaire desblocat</string> + <string name="confirmation_unmuted">Utilizaire sortit del silenci</string> + <string name="hint_domain">Quina instància ?</string> + <string name="hint_compose">A de qué pensatz ?</string> + <string name="hint_content_warning">Avís de contengut</string> + <string name="hint_display_name">Nom visible</string> + <string name="hint_note">Biografia</string> + <string name="hint_search">Cercar…</string> + <string name="search_no_results">I a pas cap de resulat</string> + <string name="label_quick_reply">Responsa…</string> + <string name="label_header">Bandièra</string> + <string name="link_whats_an_instance">Que es una instància ?</string> + <string name="login_connection">Connexion…</string> + <string name="dialog_whats_an_instance">Aquí podètz picar l\'adreça o domini de quina que siá instància, + coma mastodont.cat, mastodon.social, icosahedron.website o + <a href="https://instances.social">fòrca mai !</a> + \n\nSi encara non avètz cap de compte, podètz picar lo nom de l’instància ont vos agradariá + anar e crear un compte enlà.\n\n + \n\nAvètz mas d’informacins a <a href="https://joinmastodon.org">joinmastodon.org</a>. + </string> + <string name="dialog_title_finishing_media_upload">Finalizacion del mandadís del mèdia</string> + <string name="dialog_message_uploading_media">Mandadís…</string> + <string name="dialog_download_image">Telecargar</string> + <string name="dialog_message_cancel_follow_request">Anullar la demandar d’abonament ?</string> + <string name="dialog_unfollow_warning">Volètz quitar de seguir aqueste compte ?</string> + <string name="dialog_delete_post_warning">Suprimir aqueste tut \?</string> + <string name="visibility_public">Publica : es visibla a la cronologia publica</string> + <string name="visibility_unlisted">Pas listada :es pas visibla a las cronologias publicas</string> + <string name="visibility_private">Solament seguidors : pas que visibla pels vòstres seguidors</string> + <string name="visibility_direct">Dirècta : pas que visibla al monde mencionats</string> + <string name="pref_title_edit_notification_settings">Modificar las notificacions</string> + <string name="pref_title_notifications_enabled">Modificar las notificacions</string> + <string name="pref_title_notification_alerts">Alèrtas</string> + <string name="pref_title_notification_alert_sound">Notificar amb un son</string> + <string name="pref_title_notification_alert_vibrate">Notificra amb una vibracion</string> + <string name="pref_title_notification_alert_light">Notificar amb lo lum led</string> + <string name="pref_title_notification_filters">Notificar me se</string> + <string name="pref_title_notification_filter_mentions">òm me menciona</string> + <string name="pref_title_notification_filter_follows">òm me sèc</string> + <string name="pref_title_notification_filter_reblogs">òm parteja mas publicacions</string> + <string name="pref_title_notification_filter_favourites">òm met mos tuts en favorit</string> + <string name="pref_title_appearance_settings">Aparéncia</string> + <string name="pref_title_app_theme">Tèma de l’app</string> + <string name="app_them_dark">Escur</string> + <string name="app_theme_light">Luminós</string> + <string name="app_theme_black">Negre</string> + <string name="app_theme_auto">Alba automatica</string> + <string name="pref_title_browser_settings">Navegador</string> + <string name="pref_title_custom_tabs">Onglets personalizats de Chrome</string> + <string name="pref_title_post_filter">Filtre de la cronologia</string> + <string name="pref_title_post_tabs">Fil principal</string> + <string name="pref_title_show_boosts">Mostrar los retuts</string> + <string name="pref_title_show_replies">Mostrar las responsas</string> + <string name="pref_title_show_media_preview">Mostrar los apercebuts</string> + <string name="pref_title_http_proxy_settings">Proxy HTTP</string> + <string name="pref_title_http_proxy_enable">Activar lo proxy HTTP</string> + <string name="pref_title_http_proxy_server">Servidor proxy HTTP</string> + <string name="pref_title_http_proxy_port">Pòrt del servidor proxy HTTP</string> + <string name="pref_default_post_privacy">Privacitat predeterminada dels tuts</string> + <string name="pref_publishing">Publicacion</string> + <string name="post_privacy_public">Publica</string> + <string name="post_privacy_unlisted">Pas listat</string> + <string name="post_privacy_followers_only">Seguidors solament</string> + <string name="pref_post_text_size">Talha de text de l\'estatut</string> + <string name="post_text_size_smallest">Mendre</string> + <string name="post_text_size_small">Pichona</string> + <string name="post_text_size_medium">Mejana</string> + <string name="post_text_size_large">Granda</string> + <string name="post_text_size_largest">Grandassa</string> + <string name="notification_mention_name">Mencions nòvas</string> + <string name="notification_mention_descriptions">Notificacions de mencions noves</string> + <string name="notification_follow_name">Seguidors nòus</string> + <string name="notification_follow_description">Notificacions de nòus seguidors</string> + <string name="notification_boost_name">Retuts</string> + <string name="notification_boost_description">Notificacions de compartiment de vòstras publicacions</string> + <string name="notification_favourite_name">Preferits</string> + <string name="notification_favourite_description">Notificacions s’agradan vòstres tuts</string> + <string name="notification_mention_format">%1$s vos mencionan</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s e %4$d mai</string> + <string name="notification_summary_medium">%1$s, %2$s e %3$s</string> + <string name="notification_summary_small">%1$s e %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d interaccion nòva</item> + <item quantity="other">%1$d interaccions nòvas</item> + </plurals> + <string name="description_account_locked">Compte blocat</string> + <string name="about_title_activity">A prepaus</string> + <string name="about_tusky_license">Tusky es programa gratuit, liure e de còdi dobèrt. Es publicat jols tèrmes de la licéncia publica generala GNU version 3. Podètz trobar les licéncia aquí : https://www.gnu.org/licenses/gpl-3.0.ca.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">Site web del projècte : https://tusky.app</string> + <string name="about_bug_feature_request_site">Rapòrts d\'errors e demandas de foncionalitats : +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Perfil de Tusky</string> + <string name="post_share_content">Partejar lo contengut del tut</string> + <string name="post_share_link">Partejar lo ligam del tut</string> + <string name="post_media_images">Imatges</string> + <string name="post_media_video">Vidèo</string> + <string name="state_follow_requested">Demanda d’abonament</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">en %1$d ans</string> + <string name="abbreviated_in_days">en %1$dd</string> + <string name="abbreviated_in_hours">en %1$dh</string> + <string name="abbreviated_in_minutes">en %1$dm</string> + <string name="abbreviated_in_seconds">en %1$ds</string> + <string name="abbreviated_years_ago">%1$d ans</string> + <string name="follows_you">Vos sèc</string> + <string name="pref_title_alway_show_sensitive_media">Mostrar lo contengut sensible (NSFW)</string> + <string name="title_media">Mèdia</string> + <string name="replying_to">En responsa a @%1$s</string> + <string name="load_more_placeholder_text">cargar mai</string> + <string name="add_account_name">Apondre un compte</string> + <string name="add_account_description">Apondre un nòu compte Mastodon</string> + <string name="action_lists">Listas</string> + <string name="title_lists">Listas</string> + <string name="compose_active_account_description">Publicar coma %1$s</string> + <string name="action_set_caption">Apondre una legenda</string> + <string name="action_remove">Levar</string> + <string name="lock_account_label">Clavar lo compte</string> + <string name="lock_account_label_description">Demanda que validetz manualament los seguidors</string> + <string name="compose_save_draft">Salvar lo borrolhon ?</string> + <string name="send_post_notification_title">Mandadís del tut…</string> + <string name="send_post_notification_error_title">Error en enviar lo tut</string> + <string name="send_post_notification_channel_name">Mandadís dels tuts</string> + <string name="send_post_notification_cancel_title">Mandadís anullat</string> + <string name="send_post_notification_saved_content">Una còpia del tut es estat salvat dins los borrolhons</string> + <string name="action_compose_shortcut">Redactar</string> + <string name="error_no_custom_emojis">L’instància %1$s es pas compatibla amb los emoji personalizats</string> + <string name="emoji_style">Estil dels Emoji</string> + <string name="system_default">Çò del sistèma</string> + <string name="download_fonts">D’en primièr vos cal telecargar los emojis seguents</string> + <string name="performing_lookup_title">Recèrca…</string> + <string name="expand_collapse_all_posts">Desplegar/Plegar totes los estatuts</string> + <string name="action_open_post">Dobrir lo tut</string> + <string name="restart_required">Reaviada necessària</string> + <string name="restart_emoji">Vos caldrà reaviar Tusky per aplicar aquestes cambiaments</string> + <string name="later">Mai tard</string> + <string name="restart">Reaviar</string> + <string name="caption_systememoji">Los emoji per defaut de vòstre aparelh</string> + <string name="caption_blobmoji">Emoji basats en Blob emojis coneguts d\'Android 4.4–7.1</string> + <string name="caption_twemoji">Emoji estandards de Mastodon</string> + <string name="download_failed">Fracàs del telecargament</string> + <string name="profile_badge_bot_text">Robòt</string> + <string name="account_moved_description">%1$s mudèt los catons a :</string> + <string name="reblog_private">Partejar al public d’origina</string> + <string name="unreblog_private">Quitar de partejar</string> + <string name="license_description">Tusky content de còdis e compausants dels projèctes liures seguents :</string> + <string name="license_apache_2">Licéncia Apache License (còpia çai-jos)</string> + <string name="profile_metadata_label">Metadonada del perfil</string> + <string name="profile_metadata_add">Contengut</string> + <string name="profile_metadata_label_label">Nom</string> + <string name="profile_metadata_content_label">Contengut</string> + <string name="pref_title_absolute_time">Utilizar lo format de temps absolut</string> + <string name="label_remote_account">Las informacions çai-jos son pas lo rebat del perfil complèt de l’utilizaire. Tocatz per dobrir lo perfil complèt dins lo navigador.</string> + <string name="unpin_action">Tirar del perfil</string> + <string name="pin_action">Penjar</string> + <string name="error_network">Una error ret s’es producha. Mercés de verificar la connexion e tornar ensajar.</string> + <string name="title_public_local">Local</string> + <string name="title_direct_messages">Messatges dirèctes</string> + <string name="title_tab_preferences">Onglets</string> + <string name="title_posts_pinned">Penjats</string> + <string name="post_username_format">\@%1$s</string> + <string name="message_empty">Pas res aicí.</string> + <string name="action_unreblog">Suprimir lo partatge</string> + <string name="action_unfavourite">Suprimir lo favorit</string> + <string name="action_view_account_preferences">Preferéncias del compte</string> + <string name="action_add_tab">Ajustar un onglet</string> + <string name="action_links">Ligams</string> + <string name="action_mentions">Mencions</string> + <string name="action_hashtags">Etiquetas</string> + <string name="action_open_reblogger">Dobrir l’autor del partatge</string> + <string name="action_open_reblogged_by">Mostrar los retuts</string> + <string name="action_open_faved_by">Mostrar los favorits</string> + <string name="title_hashtags_dialog">Etiquetas</string> + <string name="title_mentions_dialog">Mencions</string> + <string name="title_links_dialog">Ligams</string> + <string name="action_open_media_n">Dobrir lo mèdia #%1$d</string> + <string name="action_open_as">Dobrir coma %1$s</string> + <string name="action_share_as">Partejar coma…</string> + <string name="download_media">Telecargar lo mèdia</string> + <string name="downloading_media">Telecargament del mèdia</string> + <string name="label_avatar">Avatar</string> + <string name="pref_title_timelines">Flux d’actualitats</string> + <string name="pref_title_timeline_filters">Filtres</string> + <string name="pref_title_language">Lenga</string> + <string name="pref_title_proxy_settings">Servidor mandatari</string> + <string name="pref_default_media_sensitivity">Totjorn marcar los mèdias coma sensibles</string> + <string name="pref_failed_to_sync">Fracàs de la sincronizacion de las preferéncias</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="pref_title_public_filter_keywords">Flux publics</string> + <string name="pref_title_thread_filter_keywords">Discutidas</string> + <string name="filter_addition_title">Ajustar un filtre</string> + <string name="filter_edit_title">Modificar un filtre</string> + <string name="filter_dialog_remove_button">Suprimir</string> + <string name="filter_dialog_update_button">Actualizar</string> + <string name="filter_add_description">Frasa de filtrar</string> + <string name="error_create_list">Creacion impossibla de la lsita</string> + <string name="error_rename_list">Impossible d’actualizar la lista</string> + <string name="error_delete_list">Supression impossibla de la lista</string> + <string name="action_create_list">Crear una lista</string> + <string name="action_rename_list">Actualizar la lista</string> + <string name="action_delete_list">Suprimir la lista</string> + <string name="hint_search_people_list">Cercar lo monde que seguètz</string> + <string name="action_add_to_list">Ajustar un compte a la lista</string> + <string name="action_remove_from_list">Suprimir aqueste compte de la lista</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Descriure lo contengut pels mal vesents (%1$d caractèr maximum)</item> + <item quantity="other">Descriure los contenguts pels mal vesents (%1$d caractèrs maximum)</item> + </plurals> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Favorit</item> + <item quantity="other"><b>%1$s</b> Favorits</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> Partatge</item> + <item quantity="other"><b>%1$s</b> Partatges</item> + </plurals> + <string name="title_reblogged_by">Partejat per</string> + <string name="title_favourited_by">Aimat per</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s e %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s e %3$d mai</string> + <string name="description_post_media">Mèdia : %1$s</string> + <string name="description_post_cw">Avertiment : %1$s</string> + <string name="description_post_media_no_description_placeholder">Cap de descripcion</string> + <string name="description_post_reblogged">Repartajat</string> + <string name="description_post_favourited">Mes en favorit</string> + <string name="description_visibility_public">Publica</string> + <string name="description_visibility_unlisted">Pas listada</string> + <string name="description_visibility_private">Seguidors</string> + <string name="description_visibility_direct">Dirècte</string> + <string name="hint_list_name">Nom de la lista</string> + <string name="edit_hashtag_hint">Etiquetas sens #</string> + <string name="compose_shortcut_long_label">Escriure una publicacion</string> + <string name="compose_shortcut_short_label">Redactar</string> + <string name="action_delete_and_redraft">Suprimir e reformular</string> + <string name="dialog_redraft_post_warning">Suprimir e reformular aqueste tut \?</string> + <string name="app_theme_system">Utilizar lo tèma sistèma</string> + <string name="notifications_clear">Suprimir</string> + <string name="notifications_apply_filter">Filtrar</string> + <string name="filter_apply">Aplicar</string> + <string name="pref_title_bot_overlay">Mostrar coma indicator pels robòts</string> + <string name="notification_clear_text">Netejar totas las notificacions d’un biais permanent \?</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s vòte</item> + <item quantity="other">%1$s votes</item> + </plurals> + <string name="poll_info_time_absolute">S’acaba a %1$s</string> + <string name="poll_info_closed">acabat</string> + <string name="poll_vote">Votar</string> + <string name="pref_title_notification_filter_poll">Los sondatges son tampats</string> + <string name="notification_poll_name">Sondatges</string> + <string name="notification_poll_description">Notificacion de sondatges tampats</string> + <string name="poll_info_format"> \u0020<!-- 15 vòtes • demòra 1 ora --> \u0020%1$s • %2$s</string> + <string name="poll_ended_voted">Un sondatge ont avètz votat es acabat</string> + <string name="poll_ended_created">Un sondatge qu’avètz creat es acabat</string> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d jorn restant</item> + <item quantity="other">%1$d jorns restant</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d ora restant</item> + <item quantity="other">%1$d oras restant</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d minuta restant</item> + <item quantity="other">%1$d minutas restant</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d segonda restant</item> + <item quantity="other">%1$d segondas restant</item> + </plurals> + <string name="compose_preview_image_description">Accions per l’imatge %1$s</string> + <string name="pref_title_animate_gif_avatars">Activar l’animacion dels avatars</string> + <string name="caption_notoemoji">Jòc d’emoji actuals de Google</string> + <string name="description_poll">Sondatge amb opcion : %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="button_continue">Contunhar</string> + <string name="button_back">Tornar</string> + <string name="button_done">Acabat</string> + <string name="report_sent_success">\@%1$s corrèctament senhalat</string> + <string name="hint_additional_info">Comentaris suplementaris</string> + <string name="report_remote_instance">Transferir a %1$s</string> + <string name="failed_report">Fracàs del senhalament</string> + <string name="failed_fetch_posts">Recuperacion dels estatuts impossibla</string> + <string name="report_description_1">Lo senhalament serà enviat a vòstre moderator de servidor. Podètz fornir una explicacion de perque senhalatz lo compte çai-jos :</string> + <string name="report_description_remote_instance">Aqueste compte es en un autre servidor. Enviar una còpia anonimizada del senhalament \?</string> + <string name="title_domain_mutes">Domenis resconduts</string> + <string name="action_view_domain_mutes">Domenis resconduts</string> + <string name="action_mute_domain">Rescondre %1$s</string> + <string name="confirmation_domain_unmuted">%1$s es pas mai rescondut</string> + <string name="mute_domain_warning">Volètz vertadièrament blocar complètament %1$s \? Veiretz pas mai de contengut venent d’aqueste domeni, nimai los flux d’actualitat o las notificacion. Vòstres seguidors d’aqueste domeni seràn tirats.</string> + <string name="mute_domain_warning_dialog_ok">Rescondre tot lo domeni</string> + <string name="filter_dialog_whole_word">Mot complèt</string> + <string name="filter_dialog_whole_word_description">Quand un mot clau o una frasa es solament alfanumeric, serà pas qu’aplicat se correspond al mot complèt</string> + <string name="pref_title_alway_open_spoiler">Totjorn desplegar las publicacions marcadas amb un avertiment de contengut</string> + <string name="title_accounts">Comptes</string> + <string name="failed_search">Fracàs de la recèrca</string> + <string name="action_add_poll">Ajustar un sondatge</string> + <string name="create_poll_title">Sondatge</string> + <string name="duration_5_min">5 minutas</string> + <string name="duration_30_min">30 minutas</string> + <string name="duration_1_hour">1 ora</string> + <string name="duration_6_hours">6 oras</string> + <string name="duration_1_day">1 jorn</string> + <string name="duration_3_days">3 jorns</string> + <string name="duration_7_days">7 jorns</string> + <string name="add_poll_choice">Ajustar d’opcions</string> + <string name="poll_allow_multiple_choices">Opcions multiplas</string> + <string name="poll_new_choice_hint">Opcion %1$d</string> + <string name="edit_poll">Modificar</string> + <string name="title_scheduled_posts">Tuts planificats</string> + <string name="action_edit">Modificar</string> + <string name="action_access_scheduled_posts">Tuts planificats</string> + <string name="action_schedule_post">Planificar de tuts</string> + <string name="action_reset_schedule">Escafar</string> + <string name="post_lookup_error_format">Error en cercant la publicacion %1$s</string> + <string name="about_powered_by_tusky">Propulsat per Tusky</string> + <string name="title_bookmarks">Marcapaginas</string> + <string name="action_bookmark">Ajustar als marcapaginas</string> + <string name="action_view_bookmarks">Marcapaginas</string> + <string name="description_post_bookmarked">Ajustat als marcapaginas</string> + <string name="select_list_title">Seleccionar la list</string> + <string name="list">Lista</string> + <string name="no_drafts">Avètz pas cap de borrolhon.</string> + <string name="no_scheduled_posts">Avètz pas cap de tut planificat.</string> + <string name="warning_scheduling_interval">L’interval minimum de planificacion sus Mastodon e de 5 minutas.</string> + <string name="notification_follow_request_name">Demandas d’abonament</string> + <string name="hashtags">Etiquetas</string> + <string name="action_unmute_desc">Amagar pas mai a %1$s</string> + <string name="notification_follow_request_format">%1$s a demandat a vos seguir</string> + <string name="pref_title_show_cards_in_timelines">Mostrar los apercebuts dels ligams</string> + <string name="notification_subscription_description">Notificacions quand qualqu’un que seguissètz publica una publicacion novèla</string> + <string name="dialog_delete_conversation_warning">Suprimir aquesta conversacion \?</string> + <string name="notification_subscription_format">%1$s ven de publicar</string> + <string name="action_unsubscribe_account">Quitar de seguir</string> + <string name="action_subscribe_account">Seguir</string> + <string name="draft_deleted">Borrolhon suprimit</string> + <string name="drafts_failed_loading_reply">Fracàs del cargament de las info de responsa</string> + <string name="drafts_post_failed_to_send">Fracàs de l’enviament !</string> + <string name="dialog_delete_list_warning">Volètz vertadièrament suprimir la lista %1$s \?</string> + <string name="review_notifications">Repassar las notificacions</string> + <string name="account_note_saved">Enregistrat !</string> + <string name="account_note_hint">Vòstra nòta privada tocant aqueste compte</string> + <string name="no_announcements">I a pas cap d’anóncia.</string> + <string name="duration_indefinite">Infinit</string> + <string name="label_duration">Durada</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s persona</item> + <item quantity="other">%1$s personas</item> + </plurals> + <string name="add_hashtag_title">Apondre hashtag</string> + <string name="post_media_attachments">Pèças juntas</string> + <string name="post_media_audio">Àudio</string> + <string name="notification_subscription_name">Publicacions novèlas</string> + <string name="notification_follow_request_description">Notificacions de demandas de seguiment</string> + <string name="pref_main_nav_position_option_bottom">Enbàs</string> + <string name="pref_main_nav_position_option_top">Ennaut</string> + <string name="pref_main_nav_position">Posicion de navigacion principala</string> + <string name="pref_title_animate_custom_emojis">Emoji animats personalizats</string> + <string name="pref_title_gradient_for_media">Afichar un degradat colorat pels mèdias amagats</string> + <string name="pref_title_notification_filter_subscriptions">qualqu’un que seguissi a publicat una publicacion novèla</string> + <string name="pref_title_notification_filter_follow_requests">abonament demandat</string> + <string name="dialog_mute_warning">Amagar @%1$s \?</string> + <string name="dialog_block_warning">Blocar @%1$s \?</string> + <string name="dialog_mute_hide_notifications">Amagar las notificacions</string> + <string name="action_unmute_conversation">Amagar pas mai la conversacion</string> + <string name="action_mute_conversation">Amagar la conversacion</string> + <string name="action_unmute_domain">Amagar pas mai a %1$s</string> + <string name="action_delete_conversation">Suprimir la conversacion</string> + <string name="title_announcements">Anóncias</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Podètz pas enviar mai de %1$d pèça junta.</item> + <item quantity="other">Podètz pas enviar mai de %1$d pèças juntas.</item> + </plurals> + <string name="wellbeing_hide_stats_profile">Amagar las estatisticas dels perfils</string> + <string name="wellbeing_hide_stats_posts">Amagar las estatisticas dels tuts</string> + <string name="limit_notifications">Limitar las notificacions de la cronologia</string> + <string name="pref_title_hide_top_toolbar">Amagar lo títol ennaut de la barra</string> + <string name="pref_title_confirm_reblogs">Afichar una confirmacion abans de partejar</string> + <string name="action_unbookmark">Tirar dels marcapaginas</string> + <string name="action_add_reaction">apondre una reaccion</string> + <string name="notification_sign_up_name">Comptes novèls</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="failed_to_pin">Fracàs en penjant</string> + <string name="failed_to_unpin">Fracàs en despenjant</string> + <string name="description_post_language">Lenga de la publicacion</string> + <string name="title_login">Se desconnectar</string> + <string name="pref_show_self_username_always">Totjorn</string> + <string name="pref_show_self_username_never">Jamai</string> + <string name="notification_update_name">Publicacions modificadas</string> + <string name="status_count_one_plus">>1</string> + <string name="duration_14_days">14 jorns</string> + <string name="duration_30_days">30 jorns</string> + <string name="error_following_hashtag_format">Error en seguissent #%1$s</string> + <string name="error_unfollowing_hashtag_format">Error en quitant de seguir #%1$s</string> + <string name="notification_sign_up_format">%1$s a creat un compte</string> + <string name="notification_update_format">%1$s a modificat sa publicacion</string> + <string name="action_dismiss">Ignorar</string> + <string name="action_details">Detalhs</string> + <string name="action_edit_image">Modificar l’imatge</string> + <string name="instance_rule_title">%1$s règlas</string> + <string name="tusky_compose_post_quicksetting_label">Compausar una publicacion</string> + <string name="account_date_joined">Arribada del %1$s</string> + <string name="saving_draft">Enregistrament del borrolhon…</string> + <string name="error_image_edit_failed">Se podiá pas modificar l’imatge.</string> + <string name="pref_title_wellbeing_mode">Benestar</string> + <string name="duration_no_change">(Cap de modificacion)</string> + <string name="error_loading_account_details">Fracàs del cargament dels detalhs del compte</string> + <string name="error_could_not_load_login_page">Cargament impossible de la pagina de connexion.</string> + <string name="duration_60_days">60 jorns</string> + <string name="duration_90_days">90 jorns</string> + <string name="duration_180_days">180 jorns</string> + <string name="duration_365_days">365 jorns</string> + <string name="error_multimedia_size_limit">Los fichièrs video e àudio pòdon pas despassar %1$s.</string> + <string name="action_add_or_remove_from_list">Apondre o suprimir de la lista</string> + <string name="set_focus_description">Tocatz o lisatz lo cercle per causir lo punt focal que deu aparéisser sus las miniaturas.</string> + <string name="no_lists">Avètz pas cap de lista.</string> + <string name="pref_title_show_self_username">Mostrar lo nom d’utilizaire dins la barra d’aisinas</string> + <string name="pref_show_self_username_disambiguate">Quand mai d’un compte es connectat</string> + <string name="action_set_focus">Posicionar lo punt focal</string> + <string name="title_migration_relogin">Se tornar connectar per recebre las notificacions instantanèas</string> + <string name="pref_title_confirm_favourites">Demandar confirmacion abans d’apondre en favorit</string> + <string name="error_following_hashtags_unsupported">Aquesta instància pren pas en carga l’abonament a las etiquetas.</string> + <string name="title_followed_hashtags">Etiquetas seguidas</string> + <string name="confirmation_hashtag_unfollowed">#%1$s pas mai seguit</string> + <string name="pref_title_notification_filter_sign_ups">un compte novèl creat</string> + <string name="pref_title_notification_filter_updates">una publicacion ont ai agut una reaccion es modificada</string> + <string name="delete_scheduled_post_warning">Suprimir la publicacion programada \?</string> + <string name="drafts_post_reply_removed">La publicacion a la quala respondiá lo borrolhon foguèt suprimida</string> + <string name="instance_rule_info">En vos connectant acceptatz las règlas de %1$s.</string> + <string name="action_unfollow_hashtag_format">Quitar de seguir #%1$s \?</string> + <string name="status_created_at_now">ara</string> + <string name="notification_report_format">Senhalament novèl sus %1$s</string> + <string name="notification_header_report_format">%1$s a senhalat %2$s</string> + <string name="pref_title_notification_filter_reports">i a un senhalament novèl</string> + <string name="notification_report_name">Senhalaments</string> + <string name="report_category_violation">Violacion de las règlas</string> + <string name="report_category_other">Autre</string> + <string name="failed_to_add_to_list">Fracàs de l’apondon del compte a la lista</string> + <string name="failed_to_remove_from_list">Fracàs de la supression del compte de la lista</string> + <string name="notification_sign_up_description">Notificacions quand qualqu’un crèa un compte novèl</string> + <string name="notification_report_description">Notificacions a prepaus dels senhalament a la moderacion</string> + <string name="notification_update_description">Notificacions quand una publicacion ont avètz reagit es modificada</string> + <string name="tips_push_notification_migration">Reconnectatz-vos a vòstres comptes per activar las notificacions instantanèas.</string> + <string name="follow_requests_info">Malgrat que vòstre compte siá pas verrolhat, l\'equipa de %1$s a pensat que volriatz validar manualament las demandas de d\'abonament provenent d\'aqueles comptes.</string> + <string name="dialog_push_notification_migration">Per tal de recebre las notificacions per UnifiedPush, Tusky deu demandar a vòstre servidor Mastodon la permission de s\'inscriure a las notificacions. Aquò necessita una reconnexion de vòstres comptes per tal de cambiar los dreches OAuth acordats a Tusky. En utilizant l\'opcion de reconnexion aicí o dins las preferéncias de compte, vòstres borrolhons e l\'escondon seràn preservats.</string> + <string name="pref_title_enable_swipe_for_tabs">Activar lo limpament per se desplaçar demest los onglets</string> + <string name="dialog_push_notification_migration_other_accounts">Tusky pòt ara recebre las notificacions instantanèas d\'aquel compte. Pasmens, d\'autres de vòstres comptes an pas encara accèsses a las notificacions instantanèas. Basculatz sus cadun de vòstres comptes e reconnectatz-los per tal de recebre las notificacions amb UnifiedPush.</string> + <string name="compose_save_draft_loses_media">Enregistrar lo borrolhon \? (Las pèças juntas seràn enviadas tornamai quand restauretz lo borrolhon.)</string> + <string name="pref_default_post_language">Lenga de publicacion per defaut</string> + <string name="report_category_spam">Messatge indesirable</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="wellbeing_mode_notice">D\'unas informacions susceptiblas d\'afectar vòstre benestar mental seràn amagadas. S\'agís : +\n +\n - de notificacions de favorits, de partatge e de seguiment +\n - del nombre dels favorits/partatges de las publicacions +\n - de las estatisticas suls perfils +\n +\n Las notificacions « push » seràn pas afectadas, mas podètz reveire vòstras preferéncias de notificacion manualament.</string> + <string name="hint_media_description_missing">Los mèdias devon aver una descripcion.</string> + <string name="pref_title_http_proxy_port_message">Lo pòrt deu èsser entre %1$d e %2$d</string> + <string name="error_muting_hashtag_format">Fracàs en silenciant #%1$s</string> + <string name="error_unmuting_hashtag_format">Fracàs en quitant de silenciar #%1$s</string> + <string name="notification_summary_report_format">%1$s · %2$d publicacions juntadas</string> + <string name="post_edited">%1$s modificat</string> + <string name="description_post_edited">Modificada</string> + <string name="error_status_source_load">Fracàs del cargament de l’estatut a partir del servidor.</string> + <string name="mute_notifications_switch">Amudir las notificacions</string> + <string name="status_created_info">%1$s creèt</string> + <string name="title_edits">Modificacions</string> + <string name="status_edit_info">%1$s modifiquèt</string> + <string name="post_media_alt">ALT</string> + <string name="action_discard">Ignorar las modificacions</string> + <string name="action_continue_edit">Téner de modificar</string> + <string name="compose_unsaved_changes">Avètz de modificacions pas salvadas.</string> + <string name="a11y_label_loading_thread">Cargament del fil</string> + <string name="pref_title_reading_order">Òrdre de lectura</string> + <string name="pref_reading_order_oldest_first">Mai ancians en primièr</string> + <string name="pref_reading_order_newest_first">Mai recents primièr</string> + <string name="pref_summary_http_proxy_disabled">Desactivat</string> + <string name="pref_summary_http_proxy_missing"><pas definit></string> + <string name="pref_summary_http_proxy_invalid"><invalid></string> + <string name="action_share_account_link">Partejar lo ligam al compte</string> + <string name="action_share_account_username">Partejar lo nom d’utilizaire del compte</string> + <string name="send_account_link_to">Partejar l’URL del compte amb…</string> + <string name="send_account_username_to">Partejar lo nom d’utilizaire del compte amb…</string> + <string name="account_username_copied">Nom d’utilizaire copiat</string> + <string name="action_post_failed">Mandadís fracassat</string> + <string name="action_post_failed_show_drafts">Afichar los borrolhons</string> + <string name="action_post_failed_do_nothing">Ignorar</string> + <string name="action_post_failed_detail">Vòstra publicacion a pas pogut s’enviar e es estada salvada als borrolhons. +\n +\nSiá se podiá pas contactar lo servidor, siá aquel a regetat la publicacion.</string> + <string name="action_post_failed_detail_plural">Vòstras publicacions an pas pogut s’enviar e son estadas salvadas als borrolhons. +\n +\nSiá se podiá pas contactar lo servidor, siá aquel a regetat las publicacions.</string> + <string name="action_browser_login">Connexion via Navigador</string> + <string name="description_browser_login">Poiriá prendre en carga de metòdes d’autentificacions suplementaris, mas requerís un navigador compatible.</string> + <string name="description_login">Fonciona los tres quarts del temps. Cap de donadas pèrdon pas per las autras aplicacions.</string> + <string name="dialog_follow_hashtag_title">Seguir lo hashtag</string> + <string name="dialog_follow_hashtag_hint">#hashtag</string> + <string name="accessibility_talking_about_tag">%1$d personas parlan d’aqueste hashtag %2$s</string> + <string name="total_usage">Utilizacion totala</string> + <string name="total_accounts">Total de comptes</string> + <string name="title_public_trending_hashtags">Hashtags populars</string> + <string name="action_refresh">Actualizar</string> + <string name="notification_unknown_name">Desconegut</string> + <string name="socket_timeout_exception">La connexion al vòstre servidor a pres tròp de temps</string> + <string name="ui_error_bookmark">Marcatge impossibla de la publicacion : %1$s</string> + <string name="ui_error_clear_notifications">Netejatge de las notificacions fracassat : %1$s</string> + <string name="ui_success_accepted_follow_request">Demanda d’abonament acceptada</string> + <string name="ui_success_rejected_follow_request">Demanda d’abonament blocada</string> + <string name="status_filtered_show_anyway">Afichar ça que la</string> + <string name="status_filter_placeholder_label_format">Filtrat : %1$s</string> + <string name="pref_title_account_filter_keywords">Perfils</string> + <string name="ui_error_unknown">rason desconeguda</string> + <string name="hint_filter_title">Mon filtre</string> + <string name="label_filter_title">Títol</string> + <string name="filter_action_warn">Avís</string> + <string name="filter_action_hide">Rescondre</string> + <string name="filter_description_warn">Rescondre amb un avís</string> + <string name="filter_description_hide">Rescondre complètament</string> + <string name="label_filter_action">Accion del filtre</string> + <string name="label_filter_context">Contèxts del filtre</string> + <string name="action_add">Ajustar</string> + <string name="filter_keyword_display_format">%1$s (mot entièr)</string> + <string name="filter_keyword_addition_title">Ajustrar mot clau</string> + <string name="filter_edit_keyword_title">Modificar mot clau</string> + <string name="filter_description_format">%1$s : %2$s</string> + <string name="label_filter_keywords">Mots clau o frasas de filtrar</string> + <string name="post_media_image">Imatge</string> + <string name="select_list_manage">Gerir las listas</string> + <string name="ui_error_favourite">Fracàs de la mes en favorit : %1$s</string> + <string name="ui_error_reblog">Fracàs en partejant : %1$s</string> + <string name="ui_error_vote">Fracàs del vòt : %1$s</string> + <string name="ui_error_accept_follow_request">Fracàs de l’acceptacion de la demanda : %1$s</string> + <string name="ui_error_reject_follow_request">Fracàs del refús de la demanda : %1$s</string> + <string name="pref_title_show_stat_inline">Mostrar las estatisticas dins la cronologia</string> + <string name="help_empty_home">Aquò es vòstra <b>cronologia</b>. Mòstra las publicacions recentas dels comptes que seguissètz. +\n +\nPer explorar mai de compte podètz siá los descobrir dins d’autres fils, per exemple lo fil local de vòstra instància [iconics gmd_group], siá los trapar per lor nom [iconics gmd_search], per exemple « Tusky » per trobar nòstre compte Mastodon.</string> + <string name="error_missing_edits">Vòstre servidor sap qu’aquesta publicacion foguèt modificada mas ten pas cap de còpia de las modificacions doncas vos las pòt afichar. +\n +\nAquò es un problèma de <a href="https://github.com/mastodon/mastodon/issues/25398">Mastodon #25398</a>.</string> + <string name="load_newest_notifications">Cargar las notificacions mai recentas</string> + <string name="compose_delete_draft">Suprimir lo borrolhon \?</string> + <string name="pref_ui_text_size">talha tèxte UI</string> + <string name="notification_listenable_worker_name">activitat en rèireplan</string> + <string name="notification_listenable_worker_description">Notificacions quand Tuska s’executa en rèireplan</string> + <string name="notification_notification_worker">Recuperacion de las notificacions…</string> + <string name="notification_prune_cache">Manteniment del cache…</string> + <string name="about_device_info_title">Vòstre aparelh</string> + <string name="about_device_info">%1$s %2$s +\nversion d’Android : %3$s +\nversion del SDK : %4$d</string> + <string name="about_account_info_title">Vòstre compte</string> + <string name="about_account_info">\@%1$s@%2$s +\nVersion : %3$s</string> + <string name="error_media_upload_sending_fmt">Fracàs del mandadís : %1$s</string> + <string name="label_image">Imatge</string> + <string name="about_copied">Version e info aparelh copiadas</string> + <string name="about_copy">Copiar la version e las info aparelh</string> + <string name="error_media_playback">La lectura a fraccasat : %1$s</string> + <string name="dialog_delete_filter_positive_action">Suprimir</string> + <string name="dialog_delete_filter_text">Suprimir lo filtre « %1$s » \?</string> + <string name="dialog_save_profile_changes_message">Volètz enregistrar las modificacions del perfil \?</string> + <string name="title_public_trending_statuses">Publicacions tendéncia</string> + <string name="error_blocking_domain">Impossible de rescondre %1$s : %2$s</string> + <string name="list_reply_policy_none">Degun</string> + <string name="list_reply_policy_label">Mostrar las responsas a</string> + <string name="list_reply_policy_followed">Totes los utilizaires seguits</string> + <string name="list_reply_policy_list">Membres de la lista</string> + <string name="action_view_filter">Veire filtre</string> + <string name="app_theme_system_black">Utilizar lo design sistèma (negre)</string> + <string name="error_unblocking_domain">Impossible de tornar mostrar %1$s : %2$s</string> + <string name="pref_title_show_self_boosts">Mostrar los pròpris partatges</string> + <string name="pref_title_show_self_boosts_description">Qualqu’un parteja son pròpri messatge</string> + <string name="reply_sending">Mandadís…</string> + <string name="reply_sending_long">Responsa enviada.</string> + <string name="action_translate">Traduire</string> + <string name="action_show_original">Veire l’original</string> + <string name="label_translating">Traduccion…</string> + <string name="label_translated">Traduch de %1$s amb %2$s</string> + <string name="ui_error_translate">Impossible de traduire : %1$s</string> + <string name="muting_hashtag_success_format">Rescondre l’etiqueta #%1$s coma avertiment</string> + <string name="unmuting_hashtag_success_format">Afichatge de l’etiqueta #%1$s</string> + <string name="pref_title_per_timeline_preferences">Preferéncias per fil cronologic</string> + <string name="dialog_follow_warning">Seguir aqueste compte ?</string> + <string name="pref_title_confirm_follows">Afichar una confirmacion abans de seguir</string> + <string name="unknown_notification_type">Tipe de notificacion desconegut</string> + <string name="list_exclusive_label">Amagar del flux d’actualitat principal</string> + <string name="following_hashtag_success_format">Seguiment de l’etiqueta #%1$s</string> + <string name="unfollowing_hashtag_success_format">Arrèst del seguiment de l’etiqueta #%1$s</string> + <string name="pref_title_show_notifications_filter">Mostrar lo filtre de las notificacions</string> + <string name="url_copied">Url copiada</string> + <string name="confirmation_hashtag_copied">« %1$s » copiat</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-or/strings.xml b/app/src/main/res/values-or/strings.xml new file mode 100644 index 0000000..5026ac9 --- /dev/null +++ b/app/src/main/res/values-or/strings.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="title_home">ଗୃହ</string> + <string name="title_notifications">ଵିଜ୍ଞପ୍ତି</string> + <string name="post_content_show_more">ପ୍ରସାରଣ</string> + <string name="post_content_show_less">ସଙ୍କୋଚନ</string> + <string name="action_reply">ପ୍ରତ୍ୟୁତ୍ତର</string> + <string name="action_retry">ପୁନଃଚେଷ୍ଟା କରିବା</string> + <string name="action_hide_media">ମିଡ଼ିଆ ଲୁଚାଅ</string> + <string name="action_edit_own_profile">ସମ୍ପାଦନା</string> + <string name="action_undo">ପୂର୍ଵଵତ୍</string> + <string name="action_search">ସନ୍ଧାନ</string> + <string name="action_more">ଅଧିକ</string> + <string name="action_edit">ସମ୍ପାଦନା</string> + <string name="action_view_media">ମିଡ଼ିଆ</string> + <string name="action_details">ଵିଵରଣୀ</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-pa/strings.xml b/app/src/main/res/values-pa/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/main/res/values-pa/strings.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources></resources> \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml new file mode 100644 index 0000000..f67be3b --- /dev/null +++ b/app/src/main/res/values-pl/strings.xml @@ -0,0 +1,639 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Wystąpił błąd.</string> + <string name="error_empty">To nie może pozostać puste.</string> + <string name="error_invalid_domain">Wprowadzono nieprawidłową domenę</string> + <string name="error_failed_app_registration">Nie udało się uwierzytelnić z tą instancją. Jeśli problem będzie się powtarzał, spróbuj zalogować się za pomocą przeglądarki z menu aplikacji.</string> + <string name="error_no_web_browser_found">Nie znaleziono przeglądarki internetowej.</string> + <string name="error_authorization_unknown">Wystąpił nieokreślony błąd podczas próby autoryzacji. Jeśli problem będzie się powtarzał, spróbuj zalogować się za pomocą przeglądarki z menu aplikacji.</string> + <string name="error_authorization_denied">Odmówiono autoryzacji. Jeśli jesteś pewien poprawności wprowadzonych danych, spróbuj zalogowania się za pomocą przeglądarki dostępnej w menu.</string> + <string name="error_retrieving_oauth_token">Nie udało się uzyskać tokenu logowania. Jeśli problem będzie się powtarzał, spróbuj zalogować się za pomocą przeglądarki z menu aplikacji.</string> + <string name="error_compose_character_limit">Zbyt długi wpis!</string> + <string name="error_media_upload_type">Ten format pliku nie może zostać wysłany.</string> + <string name="error_media_upload_opening">Nie można otworzyć tego pliku.</string> + <string name="error_media_upload_permission">Wymagane jest pozwolenie na dostęp do plików z urządzenia.</string> + <string name="error_media_download_permission">Wymagane jest pozwolenie na przechowywanie plików na urządzeniu.</string> + <string name="error_media_upload_image_or_video">Do wpisu nie mogą być dołączone jednocześnie obrazy i pliki wideo.</string> + <string name="error_media_upload_sending">Wysyłanie nie powiodło się.</string> + <string name="error_sender_account_gone">Wysyłanie wpisu nie powiodło się.</string> + <string name="title_home">Strona główna</string> + <string name="title_notifications">Powiadomienia</string> + <string name="title_public_local">Lokalne</string> + <string name="title_public_federated">Sfederowane</string> + <string name="title_view_thread">Wątek</string> + <string name="title_posts">Wpisy</string> + <string name="title_posts_with_replies">Z odpowiedziami</string> + <string name="title_follows">Obserwowanych</string> + <string name="title_followers">Obserwujących</string> + <string name="title_favourites">Ulubione</string> + <string name="title_mutes">Wyciszeni użytkownicy</string> + <string name="title_blocks">Zablokowani użytkownicy</string> + <string name="title_follow_requests">Prośby o obserwację</string> + <string name="title_edit_profile">Edytuj profil</string> + <string name="title_drafts">Szkice</string> + <string name="title_licenses">Licencje</string> + <string name="post_boosted_format">%1$s podbił/a</string> + <string name="post_sensitive_media_title">Treści wrażliwe</string> + <string name="post_media_hidden_title">Ukryto multimedia</string> + <string name="post_sensitive_media_directions">Naciśnij, aby wyświetlić</string> + <string name="post_content_warning_show_more">Pokaż więcej</string> + <string name="post_content_warning_show_less">Pokaż mniej</string> + <string name="footer_empty">Pusto tutaj. Pociągnij, aby odświeżyć!</string> + <string name="notification_reblog_format">%1$s podbił/a Twój wpis</string> + <string name="notification_favourite_format">%1$s dodał/a Twój post do ulubionych</string> + <string name="notification_follow_format">%1$s zaczął/-ęła Cię obserwować</string> + <string name="report_username_format">Zgłoś @%1$s</string> + <string name="report_comment_hint">Dodatkowe komentarze?</string> + <string name="action_quick_reply">Szybka odpowiedź</string> + <string name="action_reply">Odpowiedz</string> + <string name="action_reblog">Podbij</string> + <string name="action_favourite">Dodaj do ulubionych</string> + <string name="action_more">Więcej</string> + <string name="action_compose">Napisz</string> + <string name="action_login">Zaloguj się z Tusky</string> + <string name="action_logout">Wyloguj się</string> + <string name="action_logout_confirm">Czy na pewno chcesz wylogować się z konta %1$s?</string> + <string name="action_follow">Obserwuj</string> + <string name="action_unfollow">Przestań obserwować</string> + <string name="action_block">Zablokuj</string> + <string name="action_unblock">Odblokuj</string> + <string name="action_hide_reblogs">Ukryj podbicia</string> + <string name="action_show_reblogs">Pokaż podbicia</string> + <string name="action_report">Zgłoś</string> + <string name="action_delete">Usuń</string> + <string name="action_send">Wyślij</string> + <string name="action_send_public">Wyślij!</string> + <string name="action_retry">Spróbuj ponownie</string> + <string name="action_close">Zamknij</string> + <string name="action_view_profile">Profil</string> + <string name="action_view_preferences">Preferencje</string> + <string name="action_view_favourites">Ulubione</string> + <string name="action_view_mutes">Wyciszeni użytkownicy</string> + <string name="action_view_blocks">Zablokowani użytkownicy</string> + <string name="action_view_follow_requests">Prośby o obserwację</string> + <string name="action_view_media">Multimedia</string> + <string name="action_open_in_web">Otwórz w przeglądarce</string> + <string name="action_add_media">Dodaj treści multimedialne</string> + <string name="action_photo_take">Wykonaj zdjęcie</string> + <string name="action_share">Udostępnij</string> + <string name="action_mute">Wycisz</string> + <string name="action_unmute">Cofnij wyciszenie</string> + <string name="action_mention">Wspomnij</string> + <string name="action_hide_media">Ukryj multimedia</string> + <string name="action_open_drawer">Otwórz szufladkę</string> + <string name="action_save">Zapisz</string> + <string name="action_edit_profile">Edytuj profil</string> + <string name="action_edit_own_profile">Edytuj</string> + <string name="action_undo">Cofnij</string> + <string name="action_accept">Akceptuj</string> + <string name="action_reject">Odrzuć</string> + <string name="action_search">Szukaj</string> + <string name="action_access_drafts">Szkice</string> + <string name="action_toggle_visibility">Widoczność wpisu</string> + <string name="action_content_warning">Ostrzeżenie o zawartości</string> + <string name="action_emoji_keyboard">Klawiatura emoji</string> + <string name="download_image">Pobieranie %1$s</string> + <string name="action_copy_link">Skopiuj odnośnik</string> + <string name="send_post_link_to">Udostępnij URL do…</string> + <string name="send_post_content_to">Udostępnij wpis do…</string> + <string name="confirmation_reported">Wyślij!</string> + <string name="confirmation_unblocked">Odblokowano użytkownika</string> + <string name="confirmation_unmuted">Cofnięto wyciszenie użytkownika</string> + <string name="hint_domain">Jaka instancja?</string> + <string name="hint_compose">Co Ci chodzi po głowie?</string> + <string name="hint_content_warning">Ostrzeżenie o zawartości</string> + <string name="hint_display_name">Nazwa wyświetlana</string> + <string name="hint_note">Biografia</string> + <string name="hint_search">Szukaj…</string> + <string name="search_no_results">Brak wyników</string> + <string name="label_quick_reply">Odpowiedz…</string> + <string name="label_avatar">Obraz profilowy</string> + <string name="label_header">Nagłówek</string> + <string name="link_whats_an_instance">Czym jest instancja?</string> + <string name="login_connection">Łączenie…</string> + <string name="dialog_whats_an_instance">Tutaj można wprowadzić domenę lub adres instancji, np. mastodon.social, icosahedron.website, social.tchncs.de, i <a href="https://instances.social">wiele więcej!</a> +\n +\nJeżeli nie posiadasz jeszcze konta, wprowadź tu nazwę instancji, na której chcesz się zarejestrować. +\n +\nInstancja jest miejscem, na którym znajduje się twoje konto, lecz możesz prosto komunikować się z ludźmi na innych instancjach i obserwować ich, tak jakbyście byli na jednej stronie. +\n +\nWięcej informacji można znaleźć na <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="dialog_title_finishing_media_upload">Kończenie wysyłania treści</string> + <string name="dialog_message_uploading_media">Wysyłanie…</string> + <string name="dialog_download_image">Pobierz</string> + <string name="dialog_message_cancel_follow_request">Czy chcesz anulować prośbę o obserwację\?</string> + <string name="dialog_unfollow_warning">Czy chcesz przestać obserwować to konto\?</string> + <string name="visibility_public">Publiczne: Opublikuj na publicznych osiach czasu</string> + <string name="visibility_unlisted">Niepubliczne: Nie wyświetlaj na publicznych osiach czasu</string> + <string name="visibility_private">Tylko obserwający: Widoczne tylko dla obserwujących</string> + <string name="visibility_direct">Bezpośrednio: Widoczne tylko dla wspomnianych</string> + <string name="pref_title_edit_notification_settings">Powiadomienia</string> + <string name="pref_title_notifications_enabled">Powiadomienia</string> + <string name="pref_title_notification_alerts">Alerty</string> + <string name="pref_title_notification_alert_sound">Powiadamiaj dźwiękiem</string> + <string name="pref_title_notification_alert_vibrate">Powiadamiaj wibracją</string> + <string name="pref_title_notification_alert_light">Powiadamiaj diodą</string> + <string name="pref_title_notification_filters">Powiadom mnie, gdy</string> + <string name="pref_title_notification_filter_mentions">wspomniano o mnie</string> + <string name="pref_title_notification_filter_follows">zaczęto mnie obserwować</string> + <string name="pref_title_notification_filter_reblogs">moje wpisy zostaną podbite</string> + <string name="pref_title_notification_filter_favourites">moje posty zostaną dodane do ulubionych</string> + <string name="pref_title_appearance_settings">Wygląd</string> + <string name="pref_title_app_theme">Motyw</string> + <string name="app_them_dark">Ciemny</string> + <string name="app_theme_light">Jasny</string> + <string name="app_theme_black">Czarny</string> + <string name="app_theme_auto">Zmieniaj automatycznie po zachodzie słońca</string> + <string name="pref_title_browser_settings">Przeglądarka</string> + <string name="pref_title_custom_tabs">Używaj niestandardowych kart Chrome</string> + <string name="pref_title_post_filter">Filtrowanie osi czasu</string> + <string name="pref_title_post_tabs">Karty</string> + <string name="pref_title_show_boosts">Pokaż podbicia</string> + <string name="pref_title_show_replies">Pokazuj odpowiedzi</string> + <string name="pref_title_show_media_preview">Pokazuj podgląd zawartości multimedialnej</string> + <string name="pref_title_http_proxy_settings">Proxy HTTP</string> + <string name="pref_title_http_proxy_enable">Włącz proxy HTTP</string> + <string name="pref_title_http_proxy_server">Serwer proxy HTTP</string> + <string name="pref_title_http_proxy_port">Port proxy HTTP</string> + <string name="pref_default_post_privacy">Domyślne ustawienia prywatności wpisów</string> + <string name="pref_publishing">Publikowanie</string> + <string name="post_privacy_public">Publiczne</string> + <string name="post_privacy_unlisted">Niewypisane</string> + <string name="post_privacy_followers_only">Tylko dla obserwujących</string> + <string name="pref_post_text_size">Rozmiar tekstu wpisów</string> + <string name="post_text_size_smallest">Najmniejszy</string> + <string name="post_text_size_small">Mały</string> + <string name="post_text_size_medium">Średni</string> + <string name="post_text_size_large">Duży</string> + <string name="post_text_size_largest">Ogromny</string> + <string name="notification_mention_name">Nowe wspomnienia</string> + <string name="notification_mention_descriptions">Powiadomienia o nowych wspomnieniach</string> + <string name="notification_follow_name">Nowi obserwujący</string> + <string name="notification_follow_description">Powiadomienia o nowych obserwujących</string> + <string name="notification_boost_name">Podbicia</string> + <string name="notification_boost_description">Powiadomienia o podbiciu wpisów</string> + <string name="notification_favourite_name">Ulubione</string> + <string name="notification_favourite_description">Powiadomienia o dodaniu wpisów do ulubionych</string> + <string name="notification_mention_format">%1$s wspomniał o Tobie</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s i %4$d innych</string> + <string name="notification_summary_medium">%1$s, %2$s, i %3$s</string> + <string name="notification_summary_small">%1$s i %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d nowa interakcja</item> + <item quantity="few">%1$d nowe interakcje</item> + <item quantity="many">%1$d nowych interakcji</item> + <item quantity="other">%1$d nowych interakcji</item> + </plurals> + <string name="description_account_locked">Konto zablokowane</string> + <string name="about_title_activity">O programie</string> + <string name="about_tusky_license">Tusky jest wolnym i otwartoźródłowym oprogramowaniem. Jest on dostępny na licencji GNU General Public License w wersji trzeciej. Możesz przeczytać przetłumaczoną treść licencji <a href="https://web.archive.org/web/20171024013739/http://itlaw.computerworld.pl/wp-content/uploads/2008/03/powszechna-licencja-publiczna-gnu_v2.pdf">tutaj</a></string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site"> Strona projektu:\n + https://tusky.app + </string> + <string name="about_bug_feature_request_site"> Zgłoszenia błędów i propozycje funkcji:\n + https://github.com/tuskyapp/Tusky/issues + </string> + <string name="about_tusky_account">Profil Tusky’ego</string> + <string name="post_share_content">Udostępnij zawartość wpisu</string> + <string name="post_share_link">Udostępnij link do postu</string> + <string name="post_media_images">Obrazy</string> + <string name="post_media_video">Wideo</string> + <string name="state_follow_requested">Wysłano prośbę o obserwację</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">w %1$d lata</string> + <string name="abbreviated_in_days">w %1$d dni</string> + <string name="abbreviated_in_hours">w %1$d godz.</string> + <string name="abbreviated_in_minutes">w %1$d min.</string> + <string name="abbreviated_in_seconds">w %1$ds</string> + <string name="abbreviated_years_ago">%1$d lat temu</string> + <string name="abbreviated_days_ago">%1$d dni temu</string> + <string name="abbreviated_hours_ago">%1$d godz. temu</string> + <string name="abbreviated_minutes_ago">%1$d min. temu</string> + <string name="abbreviated_seconds_ago">%1$ds temu</string> + <string name="follows_you">Obserwuje Cię</string> + <string name="pref_title_alway_show_sensitive_media">Zawsze wyświetlaj wrażliwą zawartość</string> + <string name="title_media">Zawartość multimedialna</string> + <string name="replying_to">Odpowiadasz na wpis autorstwa @%1$s</string> + <string name="load_more_placeholder_text">załaduj więcej</string> + <string name="add_account_name">Dodaj konto</string> + <string name="add_account_description">Dodaj nowe Konto Mastodon</string> + <string name="action_lists">Listy</string> + <string name="title_lists">Listy</string> + <string name="compose_active_account_description">Publikowanie jako %1$s</string> + <string name="action_set_caption">Ustaw podpis</string> + <string name="action_remove">Usuń</string> + <string name="lock_account_label">Zablokuj konto</string> + <string name="lock_account_label_description">Wymaga od Ciebie ręcznej akceptacji próśb o obserwacje</string> + <string name="compose_save_draft">Czy chcesz zapisać jako szkic\?</string> + <string name="send_post_notification_title">Wysyłanie wpisu…</string> + <string name="send_post_notification_error_title">Wystąpił błąd podczas wysyłania wpisu</string> + <string name="send_post_notification_channel_name">Wysyłanie wpisów</string> + <string name="send_post_notification_cancel_title">Anulowano wysyłanie</string> + <string name="send_post_notification_saved_content">Kopia wpisu została zapisana jako szkic</string> + <string name="action_compose_shortcut">Nowy wpis</string> + <string name="error_no_custom_emojis">Twoja instancja %1$s nie używa żadnych niestandardowych emoji</string> + <string name="emoji_style">Styl emoji</string> + <string name="system_default">Domyślny systemu</string> + <string name="download_fonts">Musisz najpierw pobrać te zestawy emoji</string> + <string name="performing_lookup_title">Wyszukiwanie…</string> + <string name="expand_collapse_all_posts">Rozwiń/zwiń wszystkie wpisy</string> + <string name="action_open_post">Otwórz wpis</string> + <string name="restart_required">Wymagane jest ponowne uruchomienie</string> + <string name="restart_emoji">Musisz uruchomić ponownie Tuskyego, aby zastosować zmiany</string> + <string name="later">Później</string> + <string name="restart">Uruchom ponownie</string> + <string name="caption_systememoji">Domyślny zestaw emoji tego urządzenia</string> + <string name="caption_blobmoji">Emoji Blob znane z Androida 4.4–7.1</string> + <string name="caption_twemoji">Standardowy zestaw emoji Mastodona</string> + <string name="download_failed">Pobieranie nie powiodło się</string> + <string name="account_moved_description">%1$s przeniósł się na:</string> + <string name="reblog_private">Podbij grupie docelowej autora oryginału</string> + <string name="unreblog_private">Cofnij podbicie</string> + <string name="license_description">Tusky zawiera kod i zasoby następujących projektów open source:</string> + <string name="license_apache_2">Na licencji Apache (kopia poniżej)</string> + <string name="profile_metadata_label">Metadane profilu</string> + <string name="profile_metadata_add">dodaj dane</string> + <string name="profile_metadata_label_label">Nazwa</string> + <string name="profile_metadata_content_label">Zawartość</string> + <string name="error_network">Wystąpił problem z łącznością! Sprawdź swoje połączenie internetowe i spróbuj ponownie!</string> + <string name="title_direct_messages">Bezpośrednie wiadomości</string> + <string name="title_posts_pinned">Przypięte</string> + <string name="post_content_show_more">Rozwiń</string> + <string name="post_content_show_less">Zwiń</string> + <string name="message_empty">Nic tu nie ma.</string> + <string name="action_unfavourite">Usuń z ulubionych</string> + <string name="action_delete_and_redraft">Usuń i przeredaguj</string> + <string name="action_view_account_preferences">Ustawienia konta</string> + <string name="action_links">Linki</string> + <string name="action_mentions">Wzmianki</string> + <string name="action_hashtags">Hashtagi</string> + <string name="action_open_faved_by">Pokaż ulubione</string> + <string name="title_hashtags_dialog">Hashtagi</string> + <string name="title_mentions_dialog">Wzmianki</string> + <string name="title_links_dialog">Linki</string> + <string name="action_open_as">Otwórz jako %1$s</string> + <string name="action_share_as">Udostępnij jako …</string> + <string name="title_tab_preferences">Zakładki</string> + <string name="title_domain_mutes">Ukryte domeny</string> + <string name="post_username_format">\@%1$s</string> + <string name="action_unreblog">Cofnij podbicie</string> + <string name="action_view_domain_mutes">Ukryte domeny</string> + <string name="action_add_poll">Dodaj głosowanie</string> + <string name="action_mute_domain">Wycisz %1$s</string> + <string name="action_add_tab">Dodaj zakładkę</string> + <string name="action_open_reblogger">Otwórz konto osoby podbijającej</string> + <string name="action_open_reblogged_by">Pokaż podbicia</string> + <string name="action_open_media_n">Otwórz media #%1$d</string> + <string name="download_media">Pobierz media</string> + <string name="downloading_media">Pobieranie mediów</string> + <string name="send_media_to">Wyślij media do…</string> + <string name="confirmation_domain_unmuted">Domena %1$s nie jest już schowana</string> + <string name="dialog_delete_post_warning">Usunąć ten wpis\?</string> + <string name="dialog_redraft_post_warning">Usunąć i napisać ponownie ten wpis\?</string> + <string name="mute_domain_warning">Czy jesteś pewien/pewna że chcesz zablokować wszystko z domeny %1$s\? Nie będziesz widzieć zawartości z tej domeny w żadnej osi czasu ani w twoich powiadomieniach. Twoi obserwujący z tej domeny będą usunięci.</string> + <string name="mute_domain_warning_dialog_ok">Schowaj całą domenę</string> + <string name="pref_title_notification_filter_poll">głosowania zostały zakończone</string> + <string name="pref_title_timelines">Osi czasu</string> + <string name="pref_title_timeline_filters">Filtry</string> + <string name="app_theme_system">Użyj motywu systemu</string> + <string name="pref_title_language">Język</string> + <string name="pref_title_bot_overlay">Pokaż oznaczenie dla botów</string> + <string name="pref_title_animate_gif_avatars">Animuj avatary GIF</string> + <string name="pref_title_proxy_settings">Proxy</string> + <string name="pref_default_media_sensitivity">Zawsze</string> + <string name="pref_failed_to_sync">Synchronizacja ustawień nie powiodła się</string> + <string name="notification_poll_name">Ankiety</string> + <string name="notification_poll_description">Powiadomienia o głosowaniach, które zostały zakończone</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="pref_title_alway_open_spoiler">Zawsze rozwijaj wpisy z ostrzeżeniami o zawartości</string> + <string name="pref_title_public_filter_keywords">Publiczne osi czasu</string> + <string name="pref_title_thread_filter_keywords">Konwersacje</string> + <string name="filter_addition_title">Dodaj filtr</string> + <string name="filter_edit_title">Edytuj filtr</string> + <string name="filter_dialog_remove_button">Usuń</string> + <string name="filter_dialog_update_button">Aktualizuj</string> + <string name="filter_dialog_whole_word">Całe słowo</string> + <string name="filter_dialog_whole_word_description">Kiedy słowo kluczowe lub fraza jest tylko alfanumeryczna, filtr będzie zastosowany jeśli pasuje do całego słowa</string> + <string name="filter_add_description">Fraza, która ma być filtrowana</string> + <string name="error_create_list">Tworzenie listy nie powiodło się</string> + <string name="error_rename_list">Zmiana nazwy listy nie powiodła się</string> + <string name="error_delete_list">Usunięcie listy nie powiodło się</string> + <string name="action_create_list">Stwórz listę</string> + <string name="action_rename_list">Zmień nazwę listy</string> + <string name="action_delete_list">Usuń listę</string> + <string name="hint_search_people_list">Szukaj osób, których obserwujesz</string> + <string name="action_add_to_list">Dodaj konto do listy</string> + <string name="action_remove_from_list">Usuń konto z listy</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Wprowadź opis dla niewidomych i niedowidzących +\n(maksymalna długość: %1$d)</item> + <item quantity="few">Wprowadź opis dla niewidomych i niedowidzących +\n(maksymalna długość: %1$d)</item> + <item quantity="many">Wprowadź opis dla niewidomych i niedowidzących +\n(maksymalna długość: %1$d)</item> + <item quantity="other">Wprowadź opis dla niewidomych i niedowidzących +\n(maksymalna długość: %1$d)</item> + </plurals> + <string name="caption_notoemoji">Aktualny zestaw emoji Google</string> + <string name="profile_badge_bot_text">Bot</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="pref_title_absolute_time">Użyj czasu absolutnego</string> + <string name="label_remote_account">Informacje poniżej mogą niekompletnie przedstawiać profil użytkownika. Kliknij by otworzyć pełny profil w przeglądarce.</string> + <string name="unpin_action">Odepnij z profilu</string> + <string name="pin_action">Przypnij do profilu</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> polubienie</item> + <item quantity="few"><b>%1$s</b> polubienia</item> + <item quantity="many"><b>%1$s</b> polubień</item> + <item quantity="other"><b>%1$s</b> polubień</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> podbicie</item> + <item quantity="few"><b>%1$s</b> podbicia</item> + <item quantity="many"><b>%1$s</b> podbić</item> + </plurals> + <string name="title_reblogged_by">Podbite przez</string> + <string name="title_favourited_by">Polubione przez</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s i %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s i %3$d innych</string> + <string name="description_post_media">Media: %1$s</string> + <string name="description_post_cw">Ostrzeżenie o zawartości: %1$s</string> + <string name="description_post_media_no_description_placeholder">Brak opisu</string> + <string name="description_post_reblogged">Podbity</string> + <string name="description_post_favourited">Polubiony</string> + <string name="description_visibility_public">Publiczny</string> + <string name="description_visibility_unlisted">Niewidoczne</string> + <string name="description_visibility_private">Obserwujący</string> + <string name="description_visibility_direct">Bezpośrednio</string> + <string name="description_poll">Głosowanie z opcjami: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="hint_list_name">Nazwa listy</string> + <string name="edit_hashtag_hint">Hashtag bez #</string> + <string name="notifications_clear">Wyczyść</string> + <string name="notifications_apply_filter">Filtr</string> + <string name="filter_apply">Zastosuj</string> + <string name="compose_shortcut_long_label">Stwórz wpis</string> + <string name="compose_shortcut_short_label">Nowy wpis</string> + <string name="notification_clear_text">Czy jesteś pewien/pewna, że chcesz wyczyścić wszystkie swoje powiadomienia\?</string> + <string name="compose_preview_image_description">Opcje dla obrazu %1$s</string> + <string name="poll_info_format"> <!-- 15 votes • 1 hour left --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s głos</item> + <item quantity="few">%1$s głosy</item> + <item quantity="many">%1$s głosów</item> + <item quantity="other">%1$s głosów</item> + </plurals> + <string name="poll_info_time_absolute">kończy się %1$s</string> + <string name="poll_info_closed">zakończone</string> + <string name="poll_vote">Głosuj</string> + <string name="poll_ended_voted">Głosowanie w którym brałeś(-aś) udział zakończyła się</string> + <string name="poll_ended_created">Ankieta, którą stworzyłeś(aś), zakończyła się</string> + <plurals name="poll_timespan_days"> + <item quantity="one">Został %1$d dzień</item> + <item quantity="few">Zostało %1$d dni</item> + <item quantity="many">Zostało %1$d dni</item> + <item quantity="other">Zostało %1$d dni</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">Została %1$d godzina</item> + <item quantity="few">Zostało %1$d godziny</item> + <item quantity="many">Zostało %1$d godzin</item> + <item quantity="other">Zostało %1$d godzin</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">Została %1$d minuta</item> + <item quantity="few">Zostało %1$d minuty</item> + <item quantity="many">Zostało %1$d minut</item> + <item quantity="other">Zostało %1$d minut</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">Została %1$d sekunda</item> + <item quantity="few">Zostało %1$d sekund</item> + <item quantity="many">Zostało %1$d sekund</item> + <item quantity="other">Zostało %1$d sekund</item> + </plurals> + <string name="button_continue">Kontynuuj</string> + <string name="button_back">Wstecz</string> + <string name="button_done">Zakończ</string> + <string name="report_sent_success">Zgłoszenie @%1$s powiodło się</string> + <string name="hint_additional_info">Dodatkowe komentarze</string> + <string name="report_remote_instance">Wyślij zgłoszenie do %1$s</string> + <string name="failed_report">Zgłoszenie nie powiodło się</string> + <string name="failed_fetch_posts">Pobieranie wpisów nie powiodło się</string> + <string name="report_description_1">Zgłoszenie zostanie przesłąne do moderatora twojego serwera. Możesz podać przyczynę zgłoszenia tego konta poniżej:</string> + <string name="report_description_remote_instance">To konto jest na innym serwerze. Czy przesłać anonimizowaną kopię zgłoszenia na ten serwer\?</string> + <string name="title_accounts">Konta</string> + <string name="failed_search">Wyszukiwanie nie powidło się</string> + <string name="create_poll_title">Głosowanie</string> + <string name="duration_5_min">5 minut</string> + <string name="duration_30_min">30 minut</string> + <string name="duration_1_hour">1 godzina</string> + <string name="duration_6_hours">6 godzin</string> + <string name="duration_1_day">1 dzień</string> + <string name="duration_3_days">3 dni</string> + <string name="duration_7_days">7 dni</string> + <string name="add_poll_choice">Dodaj wybór</string> + <string name="poll_allow_multiple_choices">Kilka wyborów</string> + <string name="poll_new_choice_hint">Opcja %1$d</string> + <string name="edit_poll">Edytuj</string> + <string name="title_scheduled_posts">Zaplanowane wpisy</string> + <string name="action_edit">Edytuj</string> + <string name="action_access_scheduled_posts">Zaplanowane wpisy</string> + <string name="action_schedule_post">Zaplanuj wpis</string> + <string name="action_reset_schedule">Resetuj</string> + <string name="about_powered_by_tusky">Napędzane przez Tusky</string> + <string name="post_lookup_error_format">Błąd przy wyszukiwaniu wpisu %1$s</string> + <string name="title_bookmarks">Zakładki</string> + <string name="action_bookmark">Dodaj do zakładek</string> + <string name="action_view_bookmarks">Zakładki</string> + <string name="description_post_bookmarked">Dodany do zakładek</string> + <string name="select_list_title">Wybierz listę</string> + <string name="list">Lista</string> + <string name="no_drafts">Nie masz żadnych szkiców.</string> + <string name="no_scheduled_posts">Nie masz żadnych zaplanowanych wpisów.</string> + <string name="warning_scheduling_interval">Mastodon umożliwia wysłanie minimalnie 5 minut od zaplanowania.</string> + <string name="notification_follow_request_name">Prośby o obserwację</string> + <string name="pref_title_confirm_reblogs">Pytaj o potwierdzenie przed podbiciem</string> + <string name="add_hashtag_title">Dodaj hashtag</string> + <string name="dialog_mute_warning">Wyciszyć @%1$s\?</string> + <string name="dialog_block_warning">Zablokować @%1$s\?</string> + <string name="action_unmute_conversation">Cofnij wyciszenie rozmowy</string> + <string name="action_mute_conversation">Wycisz rozmowę</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s osoba</item> + <item quantity="few">%1$s osoby</item> + <item quantity="many">%1$s osób</item> + <item quantity="other">%1$s osób</item> + </plurals> + <string name="hashtags">Hashtagi</string> + <string name="pref_title_show_cards_in_timelines">Pokazuj podglądy linków na osiach czasu</string> + <string name="pref_title_gradient_for_media">Pokazuj kolorowe gradienty dla ukrytej zawartości multimedialnej</string> + <string name="dialog_mute_hide_notifications">Ukryj powiadomienia</string> + <string name="action_unmute_domain">Cofnij wyciszenie %1$s</string> + <string name="action_unmute_desc">Cofnij wyciszenie %1$s</string> + <string name="pref_title_hide_top_toolbar">Ukryj tytuł górnego paska narzędzi</string> + <string name="pref_main_nav_position_option_bottom">Dół</string> + <string name="pref_main_nav_position_option_top">Góra</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Nie możesz przesłać więcej niż %1$d załącznik.</item> + <item quantity="few">Nie możesz przesłać więcej niż %1$d załączniki.</item> + <item quantity="many">Nie możesz przesłać więcej niż %1$d załączników.</item> + <item quantity="other">Nie możesz przesłać więcej niż %1$d załączników.</item> + </plurals> + <string name="drafts_failed_loading_reply">Nie udało się załadować informacji o odpowiedzi</string> + <string name="drafts_post_failed_to_send">Przesłanie wpisu nie powiodło się!</string> + <string name="dialog_delete_list_warning">Czy na pewno chcesz usunąć listę %1$s\?</string> + <string name="no_announcements">Nie ma ogłoszeń.</string> + <string name="limit_notifications">Ogranicz liczbę powiadomień o zmianach na osi czasu</string> + <string name="label_duration">Czas trwania</string> + <string name="notification_subscription_name">Nowe wpisy</string> + <string name="wellbeing_mode_notice">Niektóre informacje, które mogą wpływać na Twój dobrostan psychiczny zostaną ukryte. W ich skład wchodzą: +\n +\n - powiadomienia o ulubionych/podbiciach/obserwowaniu +\n - liczba polubień/podbić wpisu +\n - statystyki obserwujących/postów na profilach +\n +\nNie będzie to miało wpływu na powiadomienia, ale możesz zmienić ustawienia powiadomień ręcznie.</string> + <string name="pref_title_enable_swipe_for_tabs">Włącz gest przesuwania by przełączać między zakładkami</string> + <string name="post_media_attachments">Załączniki</string> + <string name="notification_follow_request_description">Powiadomienia o prośbach o obserwowanie</string> + <string name="pref_title_notification_filter_subscriptions">ktoś, kogo zasubskrybowałem/am opublikował nowy wpis</string> + <string name="pref_title_notification_filter_follow_requests">Wysłano prośbę o obserwowanie</string> + <string name="title_announcements">Ogłoszenia</string> + <string name="pref_title_wellbeing_mode">Zdrowie</string> + <string name="action_unsubscribe_account">Anuluj subskrypcję</string> + <string name="action_subscribe_account">Zasubskrybuj</string> + <string name="follow_requests_info">Mimo tego, że twoje konto nie jest zablokowane, administracja %1$s uznała, że możesz chcieć ręcznie przejrzeć te prośby o obserwację od tych kont.</string> + <string name="drafts_post_reply_removed">Wpis dla którego naszkicowałeś/aś odpowiedź został usunięty</string> + <string name="draft_deleted">Usunięto szkic</string> + <string name="wellbeing_hide_stats_profile">Ukryj ilościowe statystyki na profilach</string> + <string name="wellbeing_hide_stats_posts">Ukryj ilościowe statystyki na postach</string> + <string name="review_notifications">Przejrzyj powiadomienia</string> + <string name="account_note_saved">Zapisano!</string> + <string name="account_note_hint">Twoja prywatna notatka o tym koncie</string> + <string name="duration_indefinite">Nieograniczony</string> + <string name="post_media_audio">Dźwięk</string> + <string name="notification_subscription_description">Powiadomienia o opublikowaniu nowego wpisu przez kogoś, kogo obserwujesz</string> + <string name="pref_main_nav_position">Pozycja głównego paska nawigacji</string> + <string name="pref_title_animate_custom_emojis">Animuj niestandardowe emoji</string> + <string name="dialog_delete_conversation_warning">Usunąć tą konwersację\?</string> + <string name="action_delete_conversation">Usuń konwersację</string> + <string name="notification_subscription_format">%1$s opublikował/a post</string> + <string name="notification_follow_request_format">%1$s poprosił/a o możliwość obserwowania Cię</string> + <string name="action_unbookmark">Usuń z zakładek</string> + <string name="pref_title_confirm_favourites">Pytaj o potwierdzenie przed dodaniem do ulubionych</string> + <string name="duration_14_days">14 dni</string> + <string name="duration_30_days">30 dni</string> + <string name="duration_60_days">60 dni</string> + <string name="duration_90_days">90 dni</string> + <string name="duration_180_days">180 dni</string> + <string name="duration_365_days">365 dni</string> + <string name="tusky_compose_post_quicksetting_label">Utwórz wpis</string> + <string name="title_login">Login</string> + <string name="notification_sign_up_format">%1$s zarejestrował/a się</string> + <string name="notification_sign_up_name">Rejestracje</string> + <string name="notification_sign_up_description">Powiadomienia o nowych użytkownikach</string> + <string name="notification_update_description">Powiadomienia o edycji wpisów z którymi dokonałeś/aś interakcji</string> + <string name="pref_title_notification_filter_sign_ups">ktoś zarejestrował się</string> + <string name="pref_title_notification_filter_updates">wpis, z którym dokonałem/am interakcji został edytowany</string> + <string name="notification_update_format">%1$s edytował/a swój wpis</string> + <string name="notification_update_name">Edycje wpisów</string> + <string name="saving_draft">Zapisywanie szkicu…</string> + <string name="error_could_not_load_login_page">Nie można załadować strony logowania.</string> + <string name="dialog_push_notification_migration_other_accounts">Zalogowałeś/-aś się ponownie na swoje konto, aby przyzwolić Tusky na wysyłanie powiadomień push. Masz jednak inne konta które nie zostały zmigrowane. Przełącz się na nie i zaloguj się ponownie aby włączyć wsparcie dla powiadomień UnifiedPush.</string> + <string name="status_count_one_plus">1+</string> + <string name="account_date_joined">Dołączył/-a %1$s</string> + <string name="tips_push_notification_migration">Zaloguj się ponownie na wszystkie konta aby włączyć wsparcie dla powiadomień push.</string> + <string name="dialog_push_notification_migration">Aby użyć powiadomień push przez UnifiedPush, Tusky wymaga pozwolenia na subskrybowanie powiadomień na twoim serwerze Mastodon. Wymaga to ponownego zalogowania aby zmienić zakresy OAuth przyznane Tusky. Użycie opcji ponownego zalogowania tutaj lub w ustawieniach konta zachowa wszystkie szkice i pamięć podręczną.</string> + <string name="action_edit_image">Edytuj obraz</string> + <string name="error_image_edit_failed">Obrazek nie mógł być zmodyfikowany.</string> + <string name="title_migration_relogin">Zaloguj się ponownie aby włączyć powiadomienia push</string> + <string name="action_dismiss">Odrzuć</string> + <string name="action_details">Detale</string> + <string name="error_loading_account_details">Ładowanie informacji o koncie nie powiodło się</string> + <string name="error_multimedia_size_limit">Pliki wideo i audio nie mogą przekraczać rozmiarem %1$s MB.</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="description_post_language">Język wpisu</string> + <string name="duration_no_change">(bez zmian)</string> + <string name="error_following_hashtag_format">Wystąpił błąd podczas obserwowania #%1$s</string> + <string name="error_unfollowing_hashtag_format">Wystąpił błąd podczas odobserwowywania #%1$s</string> + <string name="set_focus_description">Naciśnij lub przeciągnij kółko, aby wybrać punkt centralny, który będzie zawsze widoczny w miniaturkach.</string> + <string name="action_add_or_remove_from_list">Dodaj lub usuń z listy</string> + <string name="failed_to_add_to_list">Nie udało się dodać konta do listy</string> + <string name="failed_to_remove_from_list">Nie udało się usunąć konta z listy</string> + <string name="action_unfollow_hashtag_format">Odobserwuj #%1$s\?</string> + <string name="pref_default_post_language">Domyślny język wpisów</string> + <string name="pref_show_self_username_never">Nigdy</string> + <string name="pref_show_self_username_always">Zawsze</string> + <string name="pref_show_self_username_disambiguate">Gdy wiele kont jest zalogowanych</string> + <string name="status_created_at_now">teraz</string> + <string name="action_add_reaction">dodaj reakcję</string> + <string name="notification_header_report_format">%1$s zgłosił/a %2$s</string> + <string name="confirmation_hashtag_unfollowed">Odobserwowano #%1$s</string> + <string name="pref_title_notification_filter_reports">jest nowe zgłoszenie</string> + <string name="notification_report_name">Zgłoszenia</string> + <string name="notification_report_description">Powiadomienia o zgłoszeniach moderacyjnych</string> + <string name="action_set_focus">Wybierz punkt centralny</string> + <string name="compose_save_draft_loses_media">Czy chcesz zapisać jako szkic\? (Załączniki zostaną załadowane ponownie po przywróceniu szkicu.)</string> + <string name="error_following_hashtags_unsupported">Ta instancja nie wspiera obserwowania hashtagów.</string> + <string name="title_followed_hashtags">Obserwowane hashtagi</string> + <string name="a11y_label_loading_thread">Ładowanie wątku</string> + <string name="instance_rule_info">Logując się akceptujesz regulamin %1$s.</string> + <string name="pref_reading_order_oldest_first">Najpierw najstarsze</string> + <string name="pref_reading_order_newest_first">Najpierw najnowsze</string> + <string name="no_lists">Nie masz żadnych list.</string> + <string name="mute_notifications_switch">Wycisz powiadomienia</string> + <string name="status_edit_info">%1$s edytował</string> + <string name="status_created_info">%1$s stworzył</string> + <string name="title_edits">Edycje</string> + <string name="compose_unsaved_changes">Masz niezapisane zmiany.</string> + <string name="instance_rule_title">%1$s regulamin</string> + <string name="action_post_failed">Błąd wysyłania</string> + <string name="action_post_failed_show_drafts">Pokaż szkice</string> + <string name="action_post_failed_do_nothing">Odrzuć</string> + <string name="post_media_alt">ALT</string> + <string name="action_browser_login">Zaloguj się przez przeglądarkę</string> + <string name="action_continue_edit">Kontynuuj edycję</string> + <string name="report_category_spam">Spam</string> + <string name="send_account_link_to">Udostępnij link do konta…</string> + <string name="send_account_username_to">Udostępnij nazwę użytkownika…</string> + <string name="account_username_copied">Skopiowano nazwę użytkownika</string> + <string name="description_post_edited">Edytowano</string> + <string name="report_category_other">Inne</string> + <string name="post_edited">Edytowano %1$s</string> + <string name="delete_scheduled_post_warning">Usunąć ten zaplanowany wpis\?</string> + <string name="pref_title_reading_order">Kolejność czytania</string> + <string name="pref_summary_http_proxy_missing"><nieustawiony></string> + <string name="pref_summary_http_proxy_invalid"><niepoprawny></string> + <string name="pref_title_http_proxy_port_message">Port powinien być pomiędzy %1$d a %2$d</string> + <string name="action_post_failed_detail">Wystąpił problem podczas publikacji posta, ale został on zapisany w szkicach. +\n +\nAlbo wystąpił problem z połączeniem z serwerem, albo serwer odrzucił post.</string> + <string name="action_post_failed_detail_plural">Wystąpił problem podczas publikacji Twojego posta i został on zapisany w szkicach. +\n +\nAlbo wystąpił problem z połączeniem z serwerem, albo serwer odrzucił Twój post.</string> + <string name="error_muting_hashtag_format">Wystąpił błąd podczas wyciszania #%1$s</string> + <string name="error_unmuting_hashtag_format">Wystąpił błąd podczas zdejmowania wyciszenia #%1$s</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="report_category_violation">Naruszenie zasad</string> + <string name="description_login">Działa w większości przypadków. Dane nie wyciekły do innych aplikacji.</string> + <string name="description_browser_login">Może obsługiwać dodatkowe formy weryfikacji, ale wymaga kompatybilnej przeglądarki.</string> + <string name="notification_report_format">Nowe zgłoszenie dotyczące %1$s</string> + <string name="notification_summary_report_format">%1$s · %2$d załączonych postów</string> + <string name="failed_to_pin">Błąd w akcji przypinania</string> + <string name="failed_to_unpin">Błąd podczas odpinania</string> + <string name="error_status_source_load">Błąd ładowania źródła statusu z serwera.</string> + <string name="pref_title_show_self_username">Wyświetl nazwy użytkowników w pasku narzędzi</string> + <string name="pref_summary_http_proxy_disabled">Wyłączone</string> + <string name="hint_media_description_missing">Media powinny mieć opis.</string> + <string name="action_discard">Odrzuć zmiany</string> + <string name="action_share_account_username">Udostępnij nazwę użytkownika</string> + <string name="action_share_account_link">Udostępnij link do konta</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml new file mode 100644 index 0000000..8fdd9c3 --- /dev/null +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -0,0 +1,626 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Ocorreu um erro.</string> + <string name="error_empty">Não pode estar vazio.</string> + <string name="error_invalid_domain">Instância inválida</string> + <string name="error_failed_app_registration">Falha na autenticação com essa instância. Se isso persistir, tente Entrar pelo Navegador no menu.</string> + <string name="error_no_web_browser_found">Nenhum navegador foi encontrado.</string> + <string name="error_authorization_unknown">Ocorreu um erro de autorização não identificado. Se isso persistir, tente Entrar pelo Navegador no menu.</string> + <string name="error_authorization_denied">A autorização foi negada. Se você tiver certeza de que forneceu as credenciais corretas, tente Entrar pelo Navegador no menu.</string> + <string name="error_retrieving_oauth_token">Falha ao obter um token de login. Se isso persistir, tente Entrar pelo Navegador no menu.</string> + <string name="error_compose_character_limit">O toot é muito longo!</string> + <string name="error_media_upload_type">Esse tipo de arquivo não pode ser enviado.</string> + <string name="error_media_upload_opening">Esse arquivo não pode ser aberto.</string> + <string name="error_media_upload_permission">Permissão para ler mídia é necessária.</string> + <string name="error_media_download_permission">Permissão para armazenar mídia é necessária.</string> + <string name="error_media_upload_image_or_video">Imagens e vídeos não podem ser anexados ao mesmo toot.</string> + <string name="error_media_upload_sending">Falha ao enviar.</string> + <string name="error_sender_account_gone">Erro ao enviar toot.</string> + <string name="title_home">Página inicial</string> + <string name="title_notifications">Notificações</string> + <string name="title_public_local">Linha local</string> + <string name="title_public_federated">Linha global</string> + <string name="title_direct_messages">Mensagens diretas</string> + <string name="title_tab_preferences">Editar abas</string> + <string name="title_view_thread">Fio</string> + <string name="title_posts">Toots</string> + <string name="title_posts_with_replies">Com respostas</string> + <string name="title_follows">Segue</string> + <string name="title_followers">Seguidores</string> + <string name="title_favourites">Favoritos</string> + <string name="title_mutes">Usuários silenciados</string> + <string name="title_blocks">Usuários bloqueados</string> + <string name="title_follow_requests">Seguidores pendentes</string> + <string name="title_edit_profile">Editar perfil</string> + <string name="title_drafts">Rascunhos</string> + <string name="title_licenses">Licenças</string> + <string name="post_boosted_format">%1$s deu boost</string> + <string name="post_sensitive_media_title">Mídia sensível</string> + <string name="post_media_hidden_title">Mídia sensível</string> + <string name="post_sensitive_media_directions">Toque para ver</string> + <string name="post_content_warning_show_more">Expandir</string> + <string name="post_content_warning_show_less">Ocultar</string> + <string name="footer_empty">Nada aqui. Arraste para atualizar!</string> + <string name="notification_reblog_format">%1$s deu boost no teu toot</string> + <string name="notification_favourite_format">%1$s favoritou teu toot</string> + <string name="notification_follow_format">%1$s te seguiu</string> + <string name="report_username_format">Denunciar @%1$s</string> + <string name="report_comment_hint">Comentários adicionais aqui</string> + <string name="action_quick_reply">Resposta rápida</string> + <string name="action_reply">Responder</string> + <string name="action_reblog">Dar boost</string> + <string name="action_unreblog">Desfazer boost</string> + <string name="action_favourite">Favoritar</string> + <string name="action_unfavourite">Desfavoritar</string> + <string name="action_more">Mais</string> + <string name="action_compose">Compor</string> + <string name="action_login">Entrar com Tusky</string> + <string name="action_logout">Sair</string> + <string name="action_logout_confirm">Tem certeza de que deseja sair de %1$s\? Isso excluirá todos os dados locais da conta, incluindo rascunhos e preferências.</string> + <string name="action_follow">Seguir</string> + <string name="action_unfollow">Deixar de seguir</string> + <string name="action_block">Bloquear</string> + <string name="action_unblock">Desbloquear</string> + <string name="action_hide_reblogs">Ocultar boosts</string> + <string name="action_show_reblogs">Mostrar boosts</string> + <string name="action_report">Denunciar</string> + <string name="action_delete">Excluir</string> + <string name="action_send">TOOT!</string> + <string name="action_send_public">TOOT!</string> + <string name="action_retry">Tentar novamente</string> + <string name="action_close">Fechar</string> + <string name="action_view_profile">Perfil</string> + <string name="action_view_preferences">Preferências</string> + <string name="action_view_favourites">Favoritos</string> + <string name="action_view_mutes">Usuários silenciados</string> + <string name="action_view_blocks">Usuários bloqueados</string> + <string name="action_view_follow_requests">Seguidores pendentes</string> + <string name="action_view_media">Mídia</string> + <string name="action_open_in_web">Abrir no navegador</string> + <string name="action_add_media">Adicionar mídia</string> + <string name="action_photo_take">Tirar foto</string> + <string name="action_share">Compartilhar</string> + <string name="action_mute">Silenciar</string> + <string name="action_unmute">Dessilenciar</string> + <string name="action_mention">Mencionar</string> + <string name="action_hide_media">Ocultar mídia</string> + <string name="action_open_drawer">Abrir menu</string> + <string name="action_save">Salvar</string> + <string name="action_edit_profile">Editar perfil</string> + <string name="action_edit_own_profile">Editar</string> + <string name="action_undo">Desfazer</string> + <string name="action_accept">Aceitar</string> + <string name="action_reject">Rejeitar</string> + <string name="action_search">Pesquisar</string> + <string name="action_access_drafts">Rascunhos</string> + <string name="action_toggle_visibility">Privacidade do toot</string> + <string name="action_content_warning">Aviso de Conteúdo</string> + <string name="action_emoji_keyboard">Teclado de emojis</string> + <string name="action_add_tab">Adicionar aba</string> + <string name="action_links">Links</string> + <string name="action_mentions">Menções</string> + <string name="action_open_reblogged_by">Mostrar boosts</string> + <string name="action_open_faved_by">Mostrar favoritos</string> + <string name="title_mentions_dialog">Menções</string> + <string name="title_links_dialog">Links</string> + <string name="download_image">Baixando %1$s</string> + <string name="action_copy_link">Copiar link</string> + <string name="action_open_as">Abrir como %1$s</string> + <string name="action_share_as">Compartilhar como…</string> + <string name="send_post_link_to">Compartilhar link do toot em…</string> + <string name="send_post_content_to">Compartilhar toot em…</string> + <string name="send_media_to">Compartilhar mídia via…</string> + <string name="confirmation_reported">Enviado!</string> + <string name="confirmation_unblocked">Usuário desbloqueado</string> + <string name="confirmation_unmuted">Usuário dessilenciado</string> + <string name="hint_domain">Qual instância?</string> + <string name="hint_compose">No que você está pensando?</string> + <string name="hint_content_warning">Aviso de Conteúdo aqui</string> + <string name="hint_display_name">Nome de exibição</string> + <string name="hint_note">Biografia</string> + <string name="hint_search">Pesquisar…</string> + <string name="search_no_results">Sem resultados</string> + <string name="label_quick_reply">Responder…</string> + <string name="label_avatar">Avatar</string> + <string name="label_header">Capa</string> + <string name="link_whats_an_instance">O que é uma instância?</string> + <string name="login_connection">Conectando…</string> + <string name="dialog_whats_an_instance">O domínio de qualquer instância pode ser inserido aqui, como mastodon.social, masto.donte.com.br, colorid.es ou qualquer <a href="https://instances.social">outro!</a> +\n +\n Se não tem uma conta ainda, insira o nome da instância que gostaria de participar e crie uma conta lá. +\n +\n Uma instância é um lugar onde sua conta é hospedada, mas é fácil se comunicar e seguir pessoas de outras instâncias como se todos estivessem no mesmo site. +\n +\n Mais informações podem ser encontradas em <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="dialog_title_finishing_media_upload">Envio de mídia terminando</string> + <string name="dialog_message_uploading_media">Enviando…</string> + <string name="dialog_download_image">Baixar</string> + <string name="dialog_message_cancel_follow_request">Cancelar solicitação para seguir\?</string> + <string name="dialog_unfollow_warning">Deixar de seguir esta conta?</string> + <string name="dialog_delete_post_warning">Excluir este toot?</string> + <string name="visibility_public">Público: Postar em linhas públicas</string> + <string name="visibility_unlisted">Não-listado: Não postar em linhas públicas</string> + <string name="visibility_private">Privado: Postar só para seguidores</string> + <string name="visibility_direct">Direto: Postar só para mencionados</string> + <string name="pref_title_edit_notification_settings">Editar notificações</string> + <string name="pref_title_notifications_enabled">Notificações</string> + <string name="pref_title_notification_alerts">Alertas</string> + <string name="pref_title_notification_alert_sound">Notificar com som</string> + <string name="pref_title_notification_alert_vibrate">Notificar com vibração</string> + <string name="pref_title_notification_alert_light">Notificar com luz</string> + <string name="pref_title_notification_filters">Notifique-me quando</string> + <string name="pref_title_notification_filter_mentions">me mencionarem</string> + <string name="pref_title_notification_filter_follows">me seguirem</string> + <string name="pref_title_notification_filter_reblogs">derem boosts nos meus toots</string> + <string name="pref_title_notification_filter_favourites">favoritarem meus toots</string> + <string name="pref_title_appearance_settings">Aparência</string> + <string name="pref_title_app_theme">Temas</string> + <string name="pref_title_timelines">Linhas do tempo</string> + <string name="app_them_dark">Escuro</string> + <string name="app_theme_light">Claro</string> + <string name="app_theme_black">AMOLED</string> + <string name="app_theme_auto">Automático</string> + <string name="app_theme_system">Usar o tema do sistema</string> + <string name="pref_title_browser_settings">Navegador</string> + <string name="pref_title_custom_tabs">Usar abas do Chrome</string> + <string name="pref_title_post_filter">Filtro da linha do tempo</string> + <string name="pref_title_post_tabs">Abas</string> + <string name="pref_title_show_boosts">Mostrar boosts</string> + <string name="pref_title_show_replies">Mostrar respostas</string> + <string name="pref_title_show_media_preview">Mostrar prévias de mídia</string> + <string name="pref_title_proxy_settings">Proxy</string> + <string name="pref_title_http_proxy_settings">Proxy HTTP</string> + <string name="pref_title_http_proxy_enable">Ativar proxy HTTP</string> + <string name="pref_title_http_proxy_server">Servidor do proxy HTTP</string> + <string name="pref_title_http_proxy_port">Porta do proxy HTTP</string> + <string name="pref_default_post_privacy">Privacidade padrão dos toots</string> + <string name="pref_default_media_sensitivity">Sempre marcar mídia como sensível</string> + <string name="pref_publishing">Toots (sincronizado com instância)</string> + <string name="pref_failed_to_sync">Erro ao sincronizar configurações</string> + <string name="post_privacy_public">Público</string> + <string name="post_privacy_unlisted">Não-listado</string> + <string name="pref_post_text_size">Tamanho da fonte</string> + <string name="post_text_size_smallest">Menor</string> + <string name="post_text_size_small">Pequeno</string> + <string name="post_text_size_medium">Médio</string> + <string name="post_text_size_large">Grande</string> + <string name="post_text_size_largest">Maior</string> + <string name="notification_mention_name">Novas menções</string> + <string name="notification_mention_descriptions">Notificações sobre menções</string> + <string name="notification_follow_name">Novos seguidores</string> + <string name="notification_follow_description">Notificações sobre novos seguidores</string> + <string name="notification_boost_name">Boosts</string> + <string name="notification_boost_description">Notificações quando derem boost em meus toots</string> + <string name="notification_favourite_name">Favoritos</string> + <string name="notification_favourite_description">Notificações quando os meus toots forem marcados como favoritos</string> + <string name="notification_mention_format">%1$s te mencionou</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s e %4$d outros</string> + <string name="notification_summary_medium">%1$s, %2$s, e %3$s</string> + <string name="notification_summary_small">%1$s e %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d nova interação</item> + <item quantity="many">%1$d novas interações</item> + <item quantity="other">%1$d novas interações</item> + </plurals> + <string name="description_account_locked">Perfil trancado</string> + <string name="about_title_activity">Sobre</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tusky é um software livre e de código aberto. Ele é licenciado sob a versão 3 da Licença Pública Geral GNU. Leia a licença aqui: https://www.gnu.org/licenses/gpl-3.0.pt-br.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">Site do projeto: https://tusky.app</string> + <string name="about_bug_feature_request_site">Reporte bugs e solicite funcionalidades: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Perfil do Tusky</string> + <string name="post_share_content">Compartilhar conteúdo do toot</string> + <string name="post_share_link">Compartilhar link do toot</string> + <string name="post_media_images">Imagens</string> + <string name="post_media_video">Vídeo</string> + <string name="state_follow_requested">Solicitação enviada</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">em %1$dy</string> + <string name="abbreviated_in_days">em %1$dd</string> + <string name="abbreviated_in_hours">em %1$dh</string> + <string name="abbreviated_in_minutes">em %1$dm</string> + <string name="abbreviated_in_seconds">em %1$ds</string> + <string name="abbreviated_years_ago">%1$da</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="follows_you">te segue</string> + <string name="pref_title_alway_show_sensitive_media">Sempre mostrar mídia sensível</string> + <string name="title_media">Mídia</string> + <string name="replying_to">Respondendo @%1$s</string> + <string name="load_more_placeholder_text">carregar mais</string> + <string name="add_account_name">Adicionar conta</string> + <string name="add_account_description">Adicionar nova conta Mastodon</string> + <string name="action_lists">Listas</string> + <string name="title_lists">Listas</string> + <string name="compose_active_account_description">Postando como %1$s</string> + <string name="action_set_caption">Descrever</string> + <string name="action_remove">Remover</string> + <string name="lock_account_label">Trancar perfil</string> + <string name="lock_account_label_description">Requer aprovação manual de seguidores</string> + <string name="compose_save_draft">Salvar rascunho?</string> + <string name="send_post_notification_title">Enviando toot…</string> + <string name="send_post_notification_error_title">Erro ao enviar toot</string> + <string name="send_post_notification_channel_name">Enviando toots</string> + <string name="send_post_notification_cancel_title">Envio cancelado</string> + <string name="send_post_notification_saved_content">Uma cópia do toot foi salva nos seus rascunhos</string> + <string name="action_compose_shortcut">Compor</string> + <string name="error_no_custom_emojis">A sua instância %1$s não possui emojis personalizados</string> + <string name="emoji_style">Estilo de emoji</string> + <string name="system_default">Padrão do sistema</string> + <string name="download_fonts">É necessário baixar estes pacotes de emojis primeiro</string> + <string name="performing_lookup_title">Carregando…</string> + <string name="expand_collapse_all_posts">Expandir/Ocultar todos os toots</string> + <string name="action_open_post">Abrir toot</string> + <string name="restart_required">É necessário reiniciar o aplicativo</string> + <string name="restart_emoji">É necessário reiniciar o aplicativo para aplicar as alterações</string> + <string name="later">Depois</string> + <string name="restart">Reiniciar</string> + <string name="caption_systememoji">Pacote de emojis padrão do seu dispositivo</string> + <string name="caption_blobmoji">Emojis padrão do Android da versão 4.4 até 7.1</string> + <string name="caption_twemoji">Pacote de emojis padrão do Mastodon</string> + <string name="download_failed">Erro ao baixar</string> + <string name="profile_badge_bot_text">Robô</string> + <string name="account_moved_description">%1$s mudou-se para:</string> + <string name="reblog_private">Dar boost para o mesmo público</string> + <string name="unreblog_private">Desfazer boost</string> + <string name="license_description">O Tusky contém código e recursos dos seguintes projetos de código aberto:</string> + <string name="license_apache_2">Licenciado sob a licença Apache (cópia abaixo)</string> + <string name="profile_metadata_label">Metadados do perfil</string> + <string name="profile_metadata_add">Adicionar</string> + <string name="profile_metadata_label_label">Rótulo</string> + <string name="profile_metadata_content_label">Conteúdo</string> + <string name="pref_title_absolute_time">Usar tempo absoluto</string> + <string name="title_reblogged_by">Levou boost de</string> + <string name="title_favourited_by">Favoritado por</string> + <string name="description_visibility_public">Público</string> + <string name="description_visibility_private">Seguidores</string> + <string name="error_network">Ocorreu um erro de rede. Por favor verifique sua conexão e tente novamente.</string> + <string name="title_posts_pinned">Fixado</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_content_show_more">Mostrar mais</string> + <string name="post_content_show_less">Mostrar menos</string> + <string name="message_empty">Nada aqui.</string> + <string name="action_delete_and_redraft">Excluir e rascunhar</string> + <string name="action_view_account_preferences">Preferências da conta</string> + <string name="action_hashtags">Hashtags</string> + <string name="action_open_reblogger">Ver quem deu boost</string> + <string name="title_hashtags_dialog">Hashtags</string> + <string name="action_open_media_n">Abrir mídia #%1$d</string> + <string name="download_media">Baixar mídia</string> + <string name="downloading_media">Baixando mídia</string> + <string name="dialog_redraft_post_warning">Excluir e rascunhar este toot\?</string> + <string name="pref_title_notification_filter_poll">enquetes terminarem</string> + <string name="pref_title_timeline_filters">Filtros</string> + <string name="pref_title_language">Idioma</string> + <string name="post_privacy_followers_only">Privado</string> + <string name="notification_poll_name">Enquetes</string> + <string name="notification_poll_description">Notificar sobre enquetes que já terminaram</string> + <string name="pref_title_public_filter_keywords">Linhas públicas</string> + <string name="pref_title_thread_filter_keywords">Conversas</string> + <string name="filter_addition_title">Criar filtro</string> + <string name="filter_edit_title">Editar filtro</string> + <string name="filter_dialog_remove_button">Excluir</string> + <string name="filter_dialog_update_button">Atualizar</string> + <string name="filter_add_description">Frase para filtrar</string> + <string name="error_create_list">Não foi possível criar a lista</string> + <string name="error_rename_list">Não foi possível atualizar a lista</string> + <string name="error_delete_list">Não foi possível excluir a lista</string> + <string name="action_create_list">Criar uma lista</string> + <string name="action_rename_list">Atualizar a lista</string> + <string name="action_delete_list">Excluir lista</string> + <string name="hint_search_people_list">Pesquisar pessoas que você segue</string> + <string name="action_add_to_list">Adicionar conta à lista</string> + <string name="action_remove_from_list">Remover conta da lista</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Descreva os conteúdos para deficientes visuais (até %1$d caractere)</item> + <item quantity="many">Descreva os conteúdos para deficientes visuais (até %1$d caracteres)</item> + <item quantity="other">Descreva os conteúdos para deficientes visuais (até %1$d caracteres)</item> + </plurals> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="label_remote_account">As informações abaixo podem refletir incompletamente o perfil do usuário. Toque aqui para abrir o perfil completo no navegador.</string> + <string name="unpin_action">Desafixar</string> + <string name="pin_action">Fixar</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Favorito</item> + <item quantity="many"><b>%1$s</b> Favoritos</item> + <item quantity="other"><b>%1$s</b> Favoritos</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> Boost</item> + <item quantity="many"><b>%1$s</b> Boosts</item> + <item quantity="other"><b>%1$s</b> Boosts</item> + </plurals> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s e %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s e %3$d outros</string> + <string name="description_post_media">Mídia: %1$s</string> + <string name="description_post_cw">Aviso de Conteúdo: %1$s</string> + <string name="description_post_media_no_description_placeholder">Sem descrição</string> + <string name="description_post_reblogged">Você deu boost</string> + <string name="description_post_favourited">Favoritado</string> + <string name="description_visibility_unlisted">Não-listado</string> + <string name="description_visibility_direct">Direto</string> + <string name="hint_list_name">Nome da lista</string> + <string name="edit_hashtag_hint">Hashtag sem #</string> + <string name="notifications_clear">Excluir notificações</string> + <string name="notifications_apply_filter">Filtrar notificações</string> + <string name="filter_apply">Salvar</string> + <string name="compose_shortcut_long_label">Compor toot</string> + <string name="compose_shortcut_short_label">Compor</string> + <string name="pref_title_bot_overlay">Mostrar indicador de robôs</string> + <string name="notification_clear_text">Tem certeza de que deseja limpar permanentemente todas as suas notificações\?</string> + <string name="compose_preview_image_description">Opções para imagem %1$s</string> + <string name="poll_info_format"> <!-- 15 votos • 1 hora restante --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s voto</item> + <item quantity="many">%1$s votos</item> + <item quantity="other">%1$s votos</item> + </plurals> + <string name="poll_info_time_absolute">termina em %1$s</string> + <string name="poll_info_closed">Terminou</string> + <string name="poll_vote">Votar</string> + <string name="poll_ended_voted">Uma enquete que você votou terminou</string> + <string name="poll_ended_created">Sua enquete terminou</string> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d dia restante</item> + <item quantity="many">%1$d dias restantes</item> + <item quantity="other">%1$d dias restantes</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d hora restante</item> + <item quantity="many">%1$d horas restantes</item> + <item quantity="other">%1$d horas restantes</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d minuto restante</item> + <item quantity="many">%1$d minutos restantes</item> + <item quantity="other">%1$d minutos restantes</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d segundo restante</item> + <item quantity="many">%1$d segundos restantes</item> + <item quantity="other">%1$d segundos restantes</item> + </plurals> + <string name="pref_title_animate_gif_avatars">Reproduzir GIFs</string> + <string name="description_poll">Enquete com as opções: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="caption_notoemoji">Pacote de emojis atual do Google</string> + <string name="button_continue">Continuar</string> + <string name="button_back">Voltar</string> + <string name="button_done">Ok</string> + <string name="report_sent_success">\@%1$s denunciado com sucesso</string> + <string name="hint_additional_info">Comentários adicionais</string> + <string name="report_remote_instance">Encaminhar para %1$s</string> + <string name="failed_report">Erro ao denunciar</string> + <string name="failed_fetch_posts">Erro ao carregar toots</string> + <string name="report_description_1">A denúncia será enviada aos moderadores da instância. Explique por que denunciou a conta:</string> + <string name="report_description_remote_instance">A conta está em outra instância. Enviar uma cópia anônima da denúncia para lá\?</string> + <string name="title_domain_mutes">Instâncias bloqueadas</string> + <string name="action_view_domain_mutes">Instâncias bloqueadas</string> + <string name="action_mute_domain">Bloquear %1$s</string> + <string name="confirmation_domain_unmuted">%1$s desbloqueada</string> + <string name="mute_domain_warning">Tem certeza de que deseja bloquear tudo de %1$s\? Você não verá mais o conteúdo desta instância em nenhuma linha do tempo pública ou nas suas notificações. Seus seguidores desta instância serão removidos.</string> + <string name="mute_domain_warning_dialog_ok">Bloquear instância</string> + <string name="filter_dialog_whole_word">Toda palavra</string> + <string name="filter_dialog_whole_word_description">Se for apenas alfanumérico, só se aplicará se combinar com a palavra inteira</string> + <string name="action_add_poll">Adicionar enquete</string> + <string name="pref_title_alway_open_spoiler">Sempre expandir toots com Aviso de Conteúdo</string> + <string name="title_accounts">Contas</string> + <string name="failed_search">Erro ao pesquisar</string> + <string name="create_poll_title">Enquete</string> + <string name="duration_5_min">5 minutos</string> + <string name="duration_30_min">30 minutos</string> + <string name="duration_1_hour">1 hora</string> + <string name="duration_6_hours">6 horas</string> + <string name="duration_1_day">1 dia</string> + <string name="duration_3_days">3 dias</string> + <string name="duration_7_days">7 dias</string> + <string name="add_poll_choice">Adicionar opção</string> + <string name="poll_allow_multiple_choices">Múltiplas opções</string> + <string name="poll_new_choice_hint">Opção %1$d</string> + <string name="edit_poll">Editar</string> + <string name="title_scheduled_posts">Agendados</string> + <string name="action_edit">Editar</string> + <string name="action_access_scheduled_posts">Agendados</string> + <string name="action_schedule_post">Agendar toot</string> + <string name="action_reset_schedule">Cancelar</string> + <string name="post_lookup_error_format">Erro ao pesquisar %1$s</string> + <string name="title_bookmarks">Salvos</string> + <string name="action_bookmark">Salvar</string> + <string name="action_view_bookmarks">Salvos</string> + <string name="about_powered_by_tusky">Desenvolvido por Tusky</string> + <string name="description_post_bookmarked">Salvo</string> + <string name="select_list_title">Selecionar lista</string> + <string name="list">Lista</string> + <string name="no_scheduled_posts">Sem toots agendados.</string> + <string name="no_drafts">Sem rascunhos.</string> + <string name="warning_scheduling_interval">Mastodon possui um intervalo mínimo de 5 minutos para agendar.</string> + <string name="notification_follow_request_name">Seguidores pendentes</string> + <string name="notification_follow_request_format">%1$s quer te seguir</string> + <string name="action_mute_conversation">Silenciar conversa</string> + <string name="action_unmute_conversation">Dessilenciar conversa</string> + <string name="dialog_block_warning">Bloquear @%1$s\?</string> + <string name="dialog_mute_warning">Silenciar @%1$s\?</string> + <string name="pref_title_notification_filter_follow_requests">quiserem me seguir</string> + <string name="notification_follow_request_description">Notificações sobre seguidores pendentes</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s pessoa</item> + <item quantity="many">%1$s pessoas</item> + <item quantity="other">%1$s pessoas</item> + </plurals> + <string name="pref_title_enable_swipe_for_tabs">Ativar deslizar para alternar entre abas</string> + <string name="pref_title_show_cards_in_timelines">Mostrar prévias de links nas linhas</string> + <string name="pref_title_confirm_reblogs">Solicitar confirmação antes de dar boost</string> + <string name="hashtags">Hashtags</string> + <string name="add_hashtag_title">Adicionar hashtag</string> + <string name="pref_title_gradient_for_media">Mostrar blur em mídia sensível</string> + <string name="pref_main_nav_position_option_bottom">Inferior</string> + <string name="pref_main_nav_position_option_top">Superior</string> + <string name="pref_main_nav_position">Posição do menu</string> + <string name="dialog_mute_hide_notifications">Ocultar notificações</string> + <string name="action_unmute_domain">Desbloquear %1$s</string> + <string name="action_unmute_desc">Dessilenciar %1$s</string> + <string name="pref_title_hide_top_toolbar">Ocultar o título da barra superior de tarefas</string> + <string name="notification_subscription_description">Notificações quando alguém que eu sigo publicar uma novo toot</string> + <string name="pref_title_notification_filter_subscriptions">alguém que eu sigo publicou um novo toot</string> + <string name="drafts_failed_loading_reply">Erro ao carregar toot para responder</string> + <string name="drafts_post_failed_to_send">Erro ao enviar o toot!</string> + <string name="drafts_post_reply_removed">O toot em que se rascunhou uma resposta foi excluído</string> + <string name="draft_deleted">Rascunho excluído</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Não é possível anexar mais de %1$d arquivo de mídia.</item> + <item quantity="many">Não é possível anexar mais de %1$d arquivos de mídia.</item> + <item quantity="other">Não é possível anexar mais de %1$d arquivos de mídia.</item> + </plurals> + <string name="wellbeing_hide_stats_profile">Ocultar status dos perfis</string> + <string name="wellbeing_hide_stats_posts">Ocultar status dos toots</string> + <string name="limit_notifications">Limitar notificações da linha do tempo</string> + <string name="review_notifications">Revisar notificações</string> + <string name="wellbeing_mode_notice">Algumas informações que podem afetar seu bem-estar serão ocultadas. Isso inclui: +\n +\n- Notificações de favoritos, boosts e seguidores +\n- Número de favoritos e boosts nos toots +\n- Status de toots e seguidores nos perfis +\n +\nNotificações push não serão afetadas, mas é possível revisar sua preferência manualmente.</string> + <string name="account_note_saved">Salvo!</string> + <string name="account_note_hint">Nota pessoal sobre este perfil aqui</string> + <string name="pref_title_wellbeing_mode">Bem-estar</string> + <string name="no_announcements">Sem comunicados.</string> + <string name="duration_indefinite">Indefinido</string> + <string name="label_duration">Duração</string> + <string name="post_media_attachments">Anexos</string> + <string name="post_media_audio">Áudio</string> + <string name="notification_subscription_name">Novos toots</string> + <string name="notification_subscription_format">%1$s recém tootou</string> + <string name="title_announcements">Comunicados</string> + <string name="follow_requests_info">Apesar do seu perfil não ser trancado, %1$s exige que você revise a solicitação para te seguir destes perfis manualmente.</string> + <string name="action_unsubscribe_account">Cancelar</string> + <string name="action_subscribe_account">Notificar</string> + <string name="pref_title_animate_custom_emojis">Animar emojis personalizados</string> + <string name="dialog_delete_conversation_warning">Excluir esta conversa\?</string> + <string name="action_delete_conversation">Excluir conversa</string> + <string name="dialog_delete_list_warning">Deseja excluir a lista %1$s\?</string> + <string name="action_unbookmark">Remover do Salvos</string> + <string name="pref_title_confirm_favourites">Solicitar confirmação antes de favoritar</string> + <string name="duration_30_days">30 dias</string> + <string name="duration_60_days">60 dias</string> + <string name="duration_90_days">90 dias</string> + <string name="duration_180_days">180 dias</string> + <string name="duration_14_days">14 dias</string> + <string name="duration_365_days">365 dias</string> + <string name="pref_summary_http_proxy_disabled">Desabilitado</string> + <string name="pref_summary_http_proxy_missing"><não definido></string> + <string name="pref_summary_http_proxy_invalid"><inválido></string> + <string name="pref_show_self_username_always">Sempre</string> + <string name="error_image_edit_failed">A imagem não pôde ser editada.</string> + <string name="error_multimedia_size_limit">Arquivos de vídeo e áudio não podem exceder %1$s MB de tamanho.</string> + <string name="hint_media_description_missing">A mídia deve ter uma descrição.</string> + <string name="pref_show_self_username_never">Nunca</string> + <string name="post_media_alt">ALT</string> + <string name="error_muting_hashtag_format">Erro ao silenciar #%1$s</string> + <string name="action_browser_login">Entrar com Navegador</string> + <string name="account_username_copied">Nome de usuário copiado</string> + <string name="title_followed_hashtags">Hashtags seguidas</string> + <string name="action_details">Detalhes</string> + <string name="error_following_hashtag_format">Erro ao seguir #%1$s</string> + <string name="error_unfollowing_hashtag_format">Erro ao deixar de seguir #%1$s</string> + <string name="error_could_not_load_login_page">Não foi possível carregar a página de login.</string> + <string name="error_loading_account_details">Falha ao carregar detalhes da conta</string> + <string name="a11y_label_loading_thread">Carregando fio</string> + <string name="instance_rule_info">Ao entrar, você concorda com as regras de %1$s.</string> + <string name="pref_title_reading_order">Ordem de leitura</string> + <string name="set_focus_description">Toque ou arraste o círculo para escolher o ponto focal que estará sempre visível nas miniaturas.</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="action_add_or_remove_from_list">Adicionar ou remover da lista</string> + <string name="dialog_push_notification_migration_other_accounts">Você se conectou novamente à sua conta atual para conceder permissão de assinatura push à Tusky. No entanto, você ainda tem outras contas que não foram migradas dessa forma. Alterne para elas e faça login novamente uma por uma para habilitar o suporte a notificações UnifiedPush.</string> + <string name="dialog_push_notification_migration">Para usar notificações push via UnifiedPush, Tusky precisa de permissão para assinar notificações em seu servidor Mastodon. Isso requer um novo login para alterar os escopos OAuth concedidos à Tusky. Usar a opção de novo login aqui ou nas preferências da conta preservará todos os seus rascunhos e cache locais.</string> + <string name="failed_to_pin">Falha ao fixar</string> + <string name="failed_to_unpin">Falha ao desafixar</string> + <string name="compose_save_draft_loses_media">Salvar rascunho\? (Os anexos serão reenviados assim que você restaurar o rascunho.)</string> + <string name="tips_push_notification_migration">Faça login novamente em todas as contas para habilitar o suporte de notificação push.</string> + <string name="status_created_info">%1$s publicou</string> + <string name="title_edits">Edições</string> + <string name="pref_default_post_language">Idioma padrão dos toots</string> + <string name="pref_title_notification_filter_reports">há uma nova denúncia</string> + <string name="description_post_language">Idioma do toot</string> + <string name="duration_no_change">(Sem alteração)</string> + <string name="tusky_compose_post_quicksetting_label">Compor toot</string> + <string name="report_category_violation">Violação de regra</string> + <string name="report_category_spam">Spam</string> + <string name="report_category_other">Outros</string> + <string name="pref_show_self_username_disambiguate">Quando várias contas estão conectadas</string> + <string name="pref_title_http_proxy_port_message">A porta deve estar entre %1$d e %2$d</string> + <string name="status_count_one_plus">1+</string> + <string name="status_created_at_now">agora</string> + <string name="action_post_failed">Falha ao enviar</string> + <string name="action_post_failed_show_drafts">Mostrar rascunhos</string> + <string name="pref_reading_order_newest_first">Mais recentes primeiro</string> + <string name="account_date_joined">Entrou em %1$s</string> + <string name="mute_notifications_switch">Silenciar notificações</string> + <string name="pref_reading_order_oldest_first">Mais antigos primeiro</string> + <string name="failed_to_add_to_list">Falha ao adicionar a conta à lista</string> + <string name="description_login">Funciona na maioria dos casos. Nenhum dado é vazado para outros aplicativos.</string> + <string name="description_browser_login">Pode oferecer suporte a métodos de autenticação adicionais, mas requer um navegador compatível.</string> + <string name="failed_to_remove_from_list">Falha ao remover a conta da lista</string> + <string name="status_edit_info">%1$s editou</string> + <string name="action_continue_edit">Continuar editando</string> + <string name="compose_unsaved_changes">Você tem alterações não salvas.</string> + <string name="description_post_edited">Editado</string> + <string name="delete_scheduled_post_warning">Excluir este toot agendado\?</string> + <string name="action_unfollow_hashtag_format">Deixar de seguir #%1$s\?</string> + <string name="action_share_account_link">Compartilhar link da conta</string> + <string name="action_share_account_username">Compartilhar nome de usuário da conta</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="post_edited">Editado %1$s</string> + <string name="no_lists">Você não tem nenhuma lista.</string> + <string name="notification_update_format">%1$s editou um toot</string> + <string name="error_following_hashtags_unsupported">Esta instância não oferece o recurso de seguir hashtags.</string> + <string name="pref_title_notification_filter_updates">um toot que eu interagi foi editado</string> + <string name="notification_sign_up_description">Notificações sobre novos usuários</string> + <string name="action_set_focus">Definir ponto de foco</string> + <string name="action_edit_image">Editar imagem</string> + <string name="pref_title_notification_filter_sign_ups">alguém se inscreveu</string> + <string name="saving_draft">Salvando rascunho…</string> + <string name="title_login">Entrar</string> + <string name="instance_rule_title">%1$s regras</string> + <string name="pref_ui_text_size">Tamanho do texto da IU</string> + <string name="accessibility_talking_about_tag">%1$d pessoas estão falando sobre a hashtag %2$s</string> + <string name="total_usage">Uso total</string> + <string name="notification_listenable_worker_name">Atividade em segundo plano</string> + <string name="notification_listenable_worker_description">Notificações quando o Tusky está trabalhando em segundo plano</string> + <string name="notification_notification_worker">Buscando notificações…</string> + <string name="about_device_info_title">Seu dispositivo</string> + <string name="about_device_info">%1$s %2$s +\nVersão Android: %3$s +\nVersão do SDK: %4$d</string> + <string name="about_account_info">\@%1$s@%2$s +\nVersão: %3$s</string> + <string name="about_account_info_title">Sua conta</string> + <string name="action_refresh">Atualizar</string> + <string name="action_add">Adicionar</string> + <string name="notification_unknown_name">Desconhecido</string> + <string name="dialog_delete_filter_text">Excluir filtro \'%1$s\'\?</string> + <string name="dialog_delete_filter_positive_action">Excluir</string> + <string name="dialog_save_profile_changes_message">Você deseja salvar as alterações do seu perfil\?</string> + <string name="about_copied">Versão e informações do dispositivo copiadas</string> + <string name="status_filter_placeholder_label_format">Filtrado: %1$s</string> + <string name="pref_title_account_filter_keywords">Perfis</string> + <string name="post_media_image">Imagem</string> + <string name="muting_hashtag_success_format">Silenciando a hashtag #%1$s com um aviso</string> + <string name="label_filter_title">Título</string> + <string name="filter_action_warn">Aviso</string> + <string name="filter_action_hide">Esconder</string> + <string name="about_copy">Copiar a versão e as informações do dispositivo</string> + <string name="load_newest_notifications">Carregar notificações mais recentes</string> + <string name="compose_delete_draft">Excluir rascunho\?</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-pt-rPT/strings.xml b/app/src/main/res/values-pt-rPT/strings.xml new file mode 100644 index 0000000..08a1883 --- /dev/null +++ b/app/src/main/res/values-pt-rPT/strings.xml @@ -0,0 +1,709 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="post_text_size_largest">Maior</string> + <string name="notification_subscription_name">Toots novos</string> + <string name="notification_sign_up_name">Criações de contas</string> + <string name="notification_summary_small">%1$s e %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d nova interação</item> + <item quantity="many">%1$d novas interações</item> + <item quantity="other">%1$d novas interações</item> + </plurals> + <string name="replying_to">A responder a @%1$s</string> + <string name="lock_account_label_description">Necessita de aprovar manualmente os seguidores</string> + <string name="compose_save_draft">Guardar rascunho\?</string> + <string name="later">Depois</string> + <string name="unpin_action">Desafixar</string> + <string name="conversation_2_recipients">%1$s e %2$s</string> + <string name="pref_title_wellbeing_mode">Bem-estar</string> + <string name="tusky_compose_post_quicksetting_label">Escrever Toot</string> + <string name="dialog_delete_list_warning">Pretende remover a lista %1$s\?</string> + <string name="follow_requests_info">Apesar do seu perfil não ser privado, %1$s exige que você reveja manualmente as solicitações para te seguir destes perfis.</string> + <string name="action_subscribe_account">Subscrever</string> + <string name="action_unsubscribe_account">Remover subscrição</string> + <string name="error_authorization_denied">Autorização negada. Se tens a certeza que introduziste as credenciais corretas, tenta fazer login através do navegador pelo menu.</string> + <string name="error_retrieving_oauth_token">Erro ao adquirir token de login. Se o erro persistir, tenta fazer login através do navegador pelo menu.</string> + <string name="error_compose_character_limit">O toot é muito extenso!</string> + <string name="error_media_upload_type">Esse tipo de ficheiro não pode ser enviado.</string> + <string name="error_media_upload_opening">Não foi possível abrir esse ficheiro.</string> + <string name="error_media_upload_permission">É necessária permissão para ler o armazenamento.</string> + <string name="error_media_download_permission">É necessária permissão para escrever no armazenamento.</string> + <string name="error_media_upload_image_or_video">Não é possível anexar imagens e vídeos no mesmo toot.</string> + <string name="error_media_upload_sending">Erro ao enviar.</string> + <string name="error_sender_account_gone">Erro ao publicar o toot.</string> + <string name="title_home">Página inicial</string> + <string name="title_notifications">Notificações</string> + <string name="title_public_local">Local</string> + <string name="title_public_federated">Federada</string> + <string name="title_direct_messages">Mensagens diretas</string> + <string name="title_tab_preferences">Separadores</string> + <string name="title_view_thread">Conversa</string> + <string name="title_posts">Toots</string> + <string name="title_posts_with_replies">Com respostas</string> + <string name="title_posts_pinned">Fixado</string> + <string name="title_follows">Segue</string> + <string name="title_followers">Seguidores</string> + <string name="title_favourites">Favoritos</string> + <string name="title_bookmarks">Itens guardados</string> + <string name="title_mutes">Utilizadores silenciados</string> + <string name="title_blocks">Utilizadores bloqueados</string> + <string name="title_domain_mutes">Instâncias bloqueadas</string> + <string name="title_follow_requests">Seguidores pendentes</string> + <string name="post_sensitive_media_title">Conteúdo sensível</string> + <string name="title_edit_profile">Edita o teu perfil</string> + <string name="post_media_hidden_title">Conteúdo multimédia ocultado</string> + <string name="title_drafts">Rascunhos</string> + <string name="post_sensitive_media_directions">Toque para ver</string> + <string name="post_content_warning_show_more">Mostrar Mais</string> + <string name="post_content_warning_show_less">Mostrar Menos</string> + <string name="post_content_show_more">Expandir</string> + <string name="post_content_show_less">Contrair</string> + <string name="title_scheduled_posts">Toots agendados</string> + <string name="title_announcements">Anúncios</string> + <string name="title_licenses">Licenças</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s deu boost</string> + <string name="message_empty">Nada aqui.</string> + <string name="footer_empty">Nada para ver aqui. Arraste para baixo para atualizar!</string> + <string name="notification_reblog_format">%1$s deu boost ao seu toot</string> + <string name="notification_favourite_format">%1$s adicionou o seu toot aos favoritos</string> + <string name="notification_follow_format">%1$s está a seguir-te</string> + <string name="notification_follow_request_format">%1$s pediu para te seguir</string> + <string name="notification_sign_up_format">%1$s criou conta</string> + <string name="notification_subscription_format">%1$s acabou de publicar um toot</string> + <string name="notification_update_format">%1$s editou um toot</string> + <string name="report_username_format">Denunciar @%1$s</string> + <string name="report_comment_hint">Comentários adicionais\?</string> + <string name="action_quick_reply">Resposta Rápida</string> + <string name="action_reply">Responder</string> + <string name="action_reblog">Fazer boost</string> + <string name="action_unreblog">Desfazer boost</string> + <string name="action_favourite">Adicionar aos favoritos</string> + <string name="action_unfavourite">Remover dos favoritos</string> + <string name="action_bookmark">Guardar</string> + <string name="action_unbookmark">Remover dos itens guardados</string> + <string name="action_more">Mais</string> + <string name="action_compose">Escrever</string> + <string name="action_login">Login com o Tusky</string> + <string name="action_logout">Terminar sessão</string> + <string name="action_logout_confirm">Tem a certeza que deseja sair da conta %1$s\? Isto vai apagar toda a informação local da conta, incluindo rascunhos e configurações.</string> + <string name="action_follow">Seguir</string> + <string name="action_unfollow">Deixar de seguir</string> + <string name="action_block">Bloquear</string> + <string name="action_unblock">Desbloquear</string> + <string name="action_hide_reblogs">Esconder boosts</string> + <string name="action_show_reblogs">Mostrar boosts</string> + <string name="action_report">Denunciar</string> + <string name="action_edit">Editar</string> + <string name="action_delete">Apagar</string> + <string name="action_delete_conversation">Apagar conversa</string> + <string name="action_delete_and_redraft">Apagar e criar novo rascunho</string> + <string name="action_send">TOOT</string> + <string name="action_send_public">TOOT!</string> + <string name="action_retry">Tentar novamente</string> + <string name="action_close">Fechar</string> + <string name="action_view_profile">Perfil</string> + <string name="action_view_preferences">Configurações</string> + <string name="action_view_account_preferences">Configurações da conta</string> + <string name="action_view_favourites">Favoritos</string> + <string name="action_view_bookmarks">Itens Guardados</string> + <string name="action_view_mutes">Utilizadores silenciados</string> + <string name="action_view_blocks">Utilizadores bloqueados</string> + <string name="action_view_domain_mutes">Instâncias bloqueadas</string> + <string name="action_view_follow_requests">Seguidores pendentes</string> + <string name="action_view_media">Conteúdo multimédia</string> + <string name="action_open_in_web">Abrir no navegador</string> + <string name="action_add_media">Adicionar conteúdo multimédia</string> + <string name="action_add_poll">Adicionar votação</string> + <string name="action_photo_take">Tirar foto</string> + <string name="action_share">Partilhar</string> + <string name="action_mute">Silenciar</string> + <string name="action_unmute">Remover silêncio</string> + <string name="action_unmute_desc">Remover %1$s do silêncio</string> + <string name="action_mute_domain">Silenciar %1$s</string> + <string name="action_unmute_domain">Remover %1$s do silêncio</string> + <string name="action_mute_conversation">Silenciar conversa</string> + <string name="action_unmute_conversation">Remover conversa do silêncio</string> + <string name="action_mention">Mencionar</string> + <string name="action_hide_media">Esconder conteúdo multimédia</string> + <string name="action_open_drawer">Abrir menu</string> + <string name="action_search">Pesquisar</string> + <string name="action_access_drafts">Rascunhos</string> + <string name="action_access_scheduled_posts">Toots Agendados</string> + <string name="action_toggle_visibility">Privacidade do toot</string> + <string name="action_content_warning">Aviso de conteúdo</string> + <string name="action_emoji_keyboard">Teclado de emojis</string> + <string name="action_schedule_post">Agendar Toot</string> + <string name="action_reset_schedule">Redefinir</string> + <string name="action_add_tab">Adicionar Separador</string> + <string name="action_links">Hiperligações</string> + <string name="action_mentions">Menções</string> + <string name="action_hashtags">Hashtags</string> + <string name="action_open_reblogger">Ver autor do boost</string> + <string name="action_open_reblogged_by">Mostrar boosts</string> + <string name="action_open_faved_by">Mostrar favoritos</string> + <string name="title_hashtags_dialog">Hashtags</string> + <string name="title_mentions_dialog">Menções</string> + <string name="title_links_dialog">Hiperligações</string> + <string name="action_open_media_n">Abrir conteúdo multimédia #%1$d</string> + <string name="download_image">A descarregar %1$s</string> + <string name="action_copy_link">Copiar a hiperligação</string> + <string name="action_open_as">Abrir como %1$s</string> + <string name="action_share_as">Partilhar como…</string> + <string name="download_media">Descarregar conteúdo multimédia</string> + <string name="downloading_media">A descarregar conteúdo multimédia</string> + <string name="send_post_link_to">Partilhar ligação da publicação via…</string> + <string name="send_post_content_to">Partilhar publicação via…</string> + <string name="send_media_to">Partilhar conteúdo multimédia via…</string> + <string name="confirmation_reported">Enviado!</string> + <string name="confirmation_unblocked">Utilizador desbloqueado</string> + <string name="confirmation_unmuted">Utilizador removido do silêncio</string> + <string name="confirmation_domain_unmuted">%1$s desbloqueada</string> + <string name="hint_domain">Que instância\?</string> + <string name="hint_compose">Em que está a pensar\?</string> + <string name="hint_content_warning">Aviso de conteúdo</string> + <string name="hint_display_name">Nome</string> + <string name="hint_note">Biografia</string> + <string name="hint_search">Pesquisar…</string> + <string name="search_no_results">Sem resultados</string> + <string name="label_quick_reply">Responder…</string> + <string name="label_avatar">Avatar</string> + <string name="label_header">Cabeçalho</string> + <string name="link_whats_an_instance">O que é uma instância\?</string> + <string name="login_connection">A ligar…</string> + <string name="dialog_whats_an_instance">O endereço ou domínio de qualquer instância pode ser introduzido aqui, como por exemplo ciberlandia.pt, masto.pt, mastodon.social ou qualquer <a href="https://instances.social">outra!</a> +\n +\nSe ainda não tens uma conta, insere o nome da instância onde pretendes participar e cria uma conta lá. +\n +\nUma instância é o local onde tua conta é criada, mas podes facilmente seguir e comunicar com pessoas de outras instâncias como se estivessem todos no mesmo site. +\n +\nMais informações disponíveis em <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="dialog_title_finishing_media_upload">A Terminar Envio de Conteúdo Multimédia</string> + <string name="dialog_message_uploading_media">A enviar…</string> + <string name="dialog_download_image">Descarregar</string> + <string name="dialog_message_cancel_follow_request">Cancelar o pedido para seguir\?</string> + <string name="dialog_unfollow_warning">Deixar de seguir esta conta\?</string> + <string name="dialog_delete_post_warning">Apagar este toot\?</string> + <string name="dialog_redraft_post_warning">Apagar e rescrever este toot\?</string> + <string name="dialog_delete_conversation_warning">Apagar esta conversa\?</string> + <string name="mute_domain_warning">Tem a certeza que pretende bloquear a instância %1$s\? Deixará de poder ver quaisquer conteúdos dessa instância em qualquer timeline pública ou nas suas notificações. Os seus seguidores dessa instância serão removidos.</string> + <string name="mute_domain_warning_dialog_ok">Bloquear instância</string> + <string name="dialog_block_warning">Bloquear @%1$s\?</string> + <string name="dialog_mute_warning">Silenciar @%1$s\?</string> + <string name="dialog_mute_hide_notifications">Esconder notificações</string> + <string name="visibility_public">Público: Publicar em timelines públicas</string> + <string name="visibility_unlisted">Não listado: Não publicar em timelines públicas</string> + <string name="visibility_private">Privado: Publicar apenas para os seguidores</string> + <string name="visibility_direct">Direto: Publicar apenas para os utilizadores mencionados</string> + <string name="pref_title_edit_notification_settings">Notificações</string> + <string name="pref_title_notifications_enabled">Notificações</string> + <string name="pref_title_notification_alerts">Alertas</string> + <string name="pref_title_notification_alert_sound">Notificar com som</string> + <string name="pref_title_notification_alert_vibrate">Notificar com vibração</string> + <string name="pref_title_notification_alert_light">Notificar com luz</string> + <string name="pref_title_notification_filters">Notifique-me quando</string> + <string name="pref_title_notification_filter_mentions">mencionado</string> + <string name="pref_title_notification_filter_follows">seguido</string> + <string name="pref_title_notification_filter_subscriptions">alguém para quem ativei os alertas criar uma publicação nova</string> + <string name="pref_title_notification_filter_follow_requests">pedido para me seguir</string> + <string name="pref_title_notification_filter_reblogs">boosts às minhas publicações</string> + <string name="pref_title_notification_filter_favourites">as minhas publicações adicionadas aos favoritos</string> + <string name="pref_title_notification_filter_poll">votações terminadas</string> + <string name="pref_title_notification_filter_sign_ups">alguém criou conta</string> + <string name="pref_title_notification_filter_updates">uma publicação com que interagi foi editada</string> + <string name="pref_title_appearance_settings">Aparência</string> + <string name="pref_title_app_theme">Tema da aplicação</string> + <string name="pref_title_timelines">Timelines</string> + <string name="pref_title_timeline_filters">Filtros</string> + <string name="app_them_dark">Escuro</string> + <string name="app_theme_light">Claro</string> + <string name="app_theme_black">AMOLED</string> + <string name="app_theme_auto">Automático ao pôr-do-sol</string> + <string name="app_theme_system">Usar o Tema do Sistema</string> + <string name="pref_title_browser_settings">Navegador</string> + <string name="pref_title_custom_tabs">Usar Separadores Personalizados do Chrome</string> + <string name="pref_title_language">Idioma</string> + <string name="pref_title_bot_overlay">Mostrar indicador para bots</string> + <string name="pref_title_animate_gif_avatars">Reproduzir avatars em GIF</string> + <string name="pref_title_gradient_for_media">Mostrar desfoque em conteúdo multimédia sensíveis</string> + <string name="pref_title_animate_custom_emojis">Animar emojis personalizados</string> + <string name="pref_title_post_filter">Filtro da timeline</string> + <string name="pref_title_post_tabs">Timeline inicial</string> + <string name="pref_title_show_boosts">Mostrar boosts</string> + <string name="pref_title_show_replies">Mostrar respostas</string> + <string name="pref_title_show_media_preview">Descarregar pré-visualização de conteúdo multimédia</string> + <string name="pref_title_proxy_settings">Proxy</string> + <string name="pref_title_http_proxy_settings">Proxy HTTP</string> + <string name="pref_title_http_proxy_enable">Ativar proxy HTTP</string> + <string name="pref_title_http_proxy_server">Servidor da proxy HTTP</string> + <string name="pref_default_post_privacy">Privacidade padrão dos toots</string> + <string name="pref_default_media_sensitivity">Classificar sempre conteúdo multimédia como sensível</string> + <string name="pref_publishing">Publicação (sincronizada com a instância)</string> + <string name="pref_failed_to_sync">Erro ao sincronizar configurações</string> + <string name="pref_main_nav_position">Posição do menu principal</string> + <string name="pref_main_nav_position_option_top">Superior</string> + <string name="pref_main_nav_position_option_bottom">Inferior</string> + <string name="post_privacy_public">Público</string> + <string name="pref_title_http_proxy_port">Porta da proxy HTTP</string> + <string name="post_privacy_unlisted">Não listado</string> + <string name="post_privacy_followers_only">Apenas seguidores</string> + <string name="post_text_size_smallest">Menor</string> + <string name="post_text_size_small">Pequeno</string> + <string name="post_text_size_medium">Médio</string> + <string name="post_text_size_large">Grande</string> + <string name="notification_mention_name">Menções novas</string> + <string name="notification_mention_descriptions">Notificações para menções novas</string> + <string name="notification_follow_name">Novos seguidores</string> + <string name="pref_post_text_size">Tamanho do texto do toot</string> + <string name="notification_follow_description">Notificações para seguidores novos</string> + <string name="notification_follow_request_name">Seguidores pendentes</string> + <string name="notification_follow_request_description">Notificações para seguidores pendentes</string> + <string name="notification_boost_name">Boosts</string> + <string name="notification_poll_name">Votações</string> + <string name="notification_poll_description">Notificações para votações terminadas</string> + <string name="notification_subscription_description">Notificações quando alguém para quem ativou os alertas publicar um toot novo</string> + <string name="notification_sign_up_description">Notificações para novos utilizadores</string> + <string name="notification_update_name">Edições de toots</string> + <string name="notification_boost_description">Notificações para boosts recebidos</string> + <string name="notification_favourite_name">Favoritos</string> + <string name="notification_favourite_description">Notificações quando os seus toots são adicionados aos favoritos</string> + <string name="notification_update_description">Notificações quando toots com os quais interagiu forem editados</string> + <string name="notification_mention_format">%1$s mencionou-te</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s e %4$d outros</string> + <string name="notification_summary_medium">%1$s, %2$s e %3$s</string> + <string name="description_account_locked">Perfil Privado</string> + <string name="about_title_activity">Sobre</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_powered_by_tusky">A correr o Tusky</string> + <string name="filter_dialog_update_button">Atualizar</string> + <string name="about_tusky_license">Tusky é um software livre e de código aberto, licenciado com a versão 3 da GNU General Public License. Pode ler a licença aqui: https://www.gnu.org/licenses/gpl-3.0.pt-br.html</string> + <string name="about_project_site">Página do projeto: https://tusky.app</string> + <string name="about_bug_feature_request_site">Reportar erros e pedidos de funcionalidades: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Perfil do Tusky</string> + <string name="post_share_content">Partilhar conteúdo do toot</string> + <string name="post_share_link">Partilhar hiperligação do toot</string> + <string name="post_media_images">Imagens</string> + <string name="post_media_video">Vídeo</string> + <string name="post_media_audio">Áudio</string> + <string name="post_media_attachments">Anexos</string> + <string name="state_follow_requested">Pedido para seguir enviado</string> + <string name="abbreviated_in_years">em %1$dy</string> + <string name="abbreviated_in_days">em %1$dd</string> + <string name="abbreviated_in_hours">em %1$dh</string> + <string name="abbreviated_in_minutes">em %1$dm</string> + <string name="abbreviated_in_seconds">em %1$ds</string> + <string name="abbreviated_years_ago">%1$dy</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="follows_you">Segue-te</string> + <string name="pref_title_alway_show_sensitive_media">Mostrar sempre conteúdo multimédia sensível</string> + <string name="pref_title_alway_open_spoiler">Expandir sempre toots com Aviso de Conteúdo</string> + <string name="filter_dialog_whole_word">Palavra completa</string> + <string name="title_media">Conteúdo Multimédia</string> + <string name="load_more_placeholder_text">carregar mais</string> + <string name="pref_title_public_filter_keywords">Timelines públicas</string> + <string name="pref_title_thread_filter_keywords">Conversas</string> + <string name="filter_addition_title">Criar filtro</string> + <string name="filter_edit_title">Editar filtro</string> + <string name="filter_dialog_remove_button">Remover</string> + <string name="filter_dialog_whole_word_description">Se a palavra ou frase for alfanumérica, só será aplicado se corresponder à palavra completa</string> + <string name="filter_add_description">Frase para filtrar</string> + <string name="add_account_name">Adicionar Conta</string> + <string name="add_account_description">Adicionar nova Conta Mastodon</string> + <string name="action_lists">Listas</string> + <string name="error_rename_list">Não foi possível atualizar a lista</string> + <string name="title_lists">Listas</string> + <string name="error_create_list">Não foi possível criar a lista</string> + <string name="error_delete_list">Não foi possível apagar a lista</string> + <string name="action_create_list">Criar uma lista</string> + <string name="action_rename_list">Atualizar a lista</string> + <string name="action_delete_list">Apagar a lista</string> + <string name="hint_search_people_list">Pesquisar pessoas que você segue</string> + <string name="action_add_to_list">Adicionar conta à lista</string> + <string name="action_remove_from_list">Remover conta da lista</string> + <string name="compose_active_account_description">Publicar com a conta %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Descrição para deficientes visuais (até %1$d letra)</item> + <item quantity="many">Descrição para deficientes visuais (até %1$d caracteres)</item> + <item quantity="other">Descrição para deficientes visuais (até %1$d caracteres)</item> + </plurals> + <string name="action_set_caption">Escrever descrição</string> + <string name="action_remove">Remover</string> + <string name="lock_account_label">Bloquear perfil</string> + <string name="send_post_notification_title">A enviar o toot…</string> + <string name="send_post_notification_error_title">Erro ao enviar o toot</string> + <string name="send_post_notification_channel_name">A Enviar os Toots</string> + <string name="send_post_notification_cancel_title">Envio cancelado</string> + <string name="send_post_notification_saved_content">Uma cópia do toot foi guardada nos seus rascunhos</string> + <string name="action_compose_shortcut">Escrever</string> + <string name="error_no_custom_emojis">A sua instância, %1$s, não tem emojis personalizados</string> + <string name="emoji_style">Estilo dos emojis</string> + <string name="system_default">Padrão do sistema</string> + <string name="download_fonts">É necessário descarregar estes pacotes de emojis primeiro</string> + <string name="performing_lookup_title">A fazer pesquisa…</string> + <string name="expand_collapse_all_posts">Expandir/Contrair todos os toots</string> + <string name="action_open_post">Abrir toot</string> + <string name="restart_required">É necessário reiniciar a aplicação</string> + <string name="restart_emoji">É necessário reiniciar o Tusky para aplicar as alterações</string> + <string name="restart">Reiniciar</string> + <string name="caption_systememoji">Pacote de emojis padrão do seu dispositivo</string> + <string name="caption_blobmoji">Emojis padrão do Android 4.4 até ao 7.1</string> + <string name="caption_twemoji">Pacote de emojis padrão do Mastodon</string> + <string name="caption_notoemoji">Pacote de emojis atuais da Google</string> + <string name="download_failed">Erro ao descarregar</string> + <string name="profile_badge_bot_text">Robô</string> + <string name="account_moved_description">%1$s mudou-se para:</string> + <string name="reblog_private">Dar boost para o público inicial</string> + <string name="unreblog_private">Desfazer boost</string> + <string name="license_description">O Tusky contém código e recursos dos seguintes projetos de código aberto:</string> + <string name="license_apache_2">Licenciado sob a licença Apache (cópia abaixo)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Metadados do perfil</string> + <string name="profile_metadata_add">adicionar dados</string> + <string name="profile_metadata_label_label">Rótulo</string> + <string name="profile_metadata_content_label">Conteúdo</string> + <string name="pref_title_absolute_time">Usar tempo absoluto</string> + <string name="label_remote_account">As informações abaixo podem refletir, de forma incompleta, o perfil do utilizador. Toque aqui para abrir o perfil completo no navegador.</string> + <string name="pin_action">Fixar</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Favorito</item> + <item quantity="many"><b>%1$s</b> Favoritos</item> + <item quantity="other"><b>%1$s</b> Favoritos</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> Boost</item> + <item quantity="many"><b>%1$s</b> Boosts</item> + <item quantity="other"><b>%1$s</b> Boosts</item> + </plurals> + <string name="title_reblogged_by">Boost dado por</string> + <string name="title_favourited_by">Adicionado aos favoritos por</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_more_recipients">%1$s, %2$s e %3$d mais</string> + <string name="description_post_media">Conteúdo multimédia: %1$s</string> + <string name="description_post_cw">Aviso de Conteúdo: %1$s</string> + <string name="description_post_media_no_description_placeholder">Sem descrição</string> + <string name="description_post_reblogged">Republicado</string> + <string name="description_post_favourited">Adicionado aos favoritos</string> + <string name="description_post_bookmarked">Guardado</string> + <string name="description_visibility_public">Pública</string> + <string name="description_visibility_unlisted">Não-listada</string> + <string name="description_visibility_private">Seguidores</string> + <string name="description_visibility_direct">Direta</string> + <string name="description_poll">Votação com as opções: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="hint_list_name">Nome da lista</string> + <string name="add_hashtag_title">Adicionar hashtag</string> + <string name="edit_hashtag_hint">Hashtag sem #</string> + <string name="hashtags">Hashtags</string> + <string name="select_list_title">Selecionar lista</string> + <string name="list">Lista</string> + <string name="notifications_clear">Apagar</string> + <string name="notifications_apply_filter">Filtrar</string> + <string name="filter_apply">Aplicar</string> + <string name="compose_shortcut_long_label">Escrever toot</string> + <string name="compose_shortcut_short_label">Escrever</string> + <string name="notification_clear_text">Tens a certeza que pretendes limpar todas as tuas notificações\?</string> + <string name="compose_preview_image_description">Ações para imagem %1$s</string> + <string name="poll_info_format"> <!-- 15 votos • 1 hora restante --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s voto</item> + <item quantity="many">%1$s votos</item> + <item quantity="other">%1$s votos</item> + </plurals> + <plurals name="poll_info_people"> + <item quantity="one">%1$s pessoa</item> + <item quantity="many">%1$s pessoas</item> + <item quantity="other">%1$s pessoas</item> + </plurals> + <string name="poll_info_time_absolute">termina em %1$s</string> + <string name="poll_info_closed">terminada</string> + <string name="poll_vote">Votar</string> + <string name="poll_ended_voted">Uma votação em que votou terminou</string> + <string name="poll_ended_created">A sua votação terminou</string> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d dia restante</item> + <item quantity="many">%1$d dias restantes</item> + <item quantity="other">%1$d dias restantes</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d hora restante</item> + <item quantity="many">%1$d horas restantes</item> + <item quantity="other">%1$d horas restantes</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d minuto restante</item> + <item quantity="many">%1$d minutos restantes</item> + <item quantity="other">%1$d minutos restantes</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d segundo restante</item> + <item quantity="many">%1$d segundos restantes</item> + <item quantity="other">%1$d segundos restantes</item> + </plurals> + <string name="button_continue">Continuar</string> + <string name="button_back">Retroceder</string> + <string name="button_done">Feito</string> + <string name="report_sent_success">\@%1$s denunciado com sucesso</string> + <string name="hint_additional_info">Comentários adicionais</string> + <string name="report_remote_instance">Encaminhar para %1$s</string> + <string name="failed_report">Erro ao denunciar</string> + <string name="failed_fetch_posts">Erro ao carregar toots</string> + <string name="report_description_1">A denúncia será enviada aos moderadores da instância. Pode adicionar abaixo uma explicação para a sua denúncia:</string> + <string name="report_description_remote_instance">A conta está noutra instância. Quer enviar uma cópia anónima da denúncia para lá\?</string> + <string name="title_accounts">Contas</string> + <string name="failed_search">Erro ao pesquisar</string> + <string name="pref_title_enable_swipe_for_tabs">Ativar gesto de deslizar para alternar entre separadores</string> + <string name="create_poll_title">Votação</string> + <string name="label_duration">Duração</string> + <string name="duration_indefinite">Indefinido</string> + <string name="duration_5_min">5 minutos</string> + <string name="duration_30_min">30 minutos</string> + <string name="duration_1_hour">1 hora</string> + <string name="duration_6_hours">6 horas</string> + <string name="duration_1_day">1 dia</string> + <string name="duration_3_days">3 dias</string> + <string name="duration_7_days">7 dias</string> + <string name="duration_14_days">14 dias</string> + <string name="duration_30_days">30 dias</string> + <string name="duration_60_days">60 dias</string> + <string name="duration_90_days">90 dias</string> + <string name="duration_180_days">180 dias</string> + <string name="duration_365_days">365 dias</string> + <string name="add_poll_choice">Adicionar opção</string> + <string name="poll_allow_multiple_choices">Escolha múltipla</string> + <string name="poll_new_choice_hint">Opção %1$d</string> + <string name="edit_poll">Editar</string> + <string name="post_lookup_error_format">Erro ao pesquisar toot %1$s</string> + <string name="no_drafts">Não tem rascunhos.</string> + <string name="no_scheduled_posts">Não tem toots agendados.</string> + <string name="account_note_saved">Guardado!</string> + <string name="wellbeing_mode_notice">Algumas informações que podem afetar seu bem-estar serão ocultadas. Isso inclui: +\n +\n- Notificações de favoritos, boosts e seguidores +\n- Número de favoritos e boosts nos toots +\n- Status de toots e seguidores nos perfis +\n +\nNotificações push não serão afetadas, mas é possível rever as configurações das notificações manualmente.</string> + <string name="review_notifications">Rever Notificações</string> + <string name="limit_notifications">Limitar notificações da timeline</string> + <string name="no_announcements">Sem anúncios.</string> + <string name="warning_scheduling_interval">O Mastodon tem um intervalo mínimo de agendamento de 5 minutos.</string> + <string name="pref_title_show_cards_in_timelines">Mostrar pré-visualização de hiperligações nas timelines</string> + <string name="pref_title_confirm_reblogs">Mostrar confirmação antes de dar boost</string> + <string name="pref_title_confirm_favourites">Mostrar confirmação antes de adicionar aos favoritos</string> + <string name="pref_title_hide_top_toolbar">Esconder o título da barra superior</string> + <string name="account_note_hint">Nota pessoal sobre este perfil</string> + <string name="wellbeing_hide_stats_posts">Esconder estatísticas quantitativas nos toots</string> + <string name="wellbeing_hide_stats_profile">Esconder estatísticas quantitativas nos perfis</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Não é possível enviar mais de %1$d arquivo de conteúdo multimédia.</item> + <item quantity="many">Não é possível enviar mais de %1$d arquivos de conteúdo multimédia.</item> + <item quantity="other">Não é possível enviar mais de %1$d arquivos de conteúdo multimédia.</item> + </plurals> + <string name="drafts_post_failed_to_send">Erro ao enviar o toot!</string> + <string name="drafts_failed_loading_reply">Erro ao carregar a informação da resposta</string> + <string name="draft_deleted">Rascunho apagado</string> + <string name="drafts_post_reply_removed">O toot para o qual escreveu um rascunho foi apagado</string> + <string name="error_generic">Ocorreu um erro.</string> + <string name="error_network">Ocorreu um erro de conetividade. Por favor, verifica a tua ligação e tenta novamente.</string> + <string name="error_empty">Isto não pode estar vazio.</string> + <string name="error_invalid_domain">A instância inserida é inválida</string> + <string name="error_failed_app_registration">Erro ao autenticar com esta instância. Se o erro persistir, tenta fazer login através do navegador pelo menu.</string> + <string name="error_no_web_browser_found">Não foi possível encontrar um navegador.</string> + <string name="error_authorization_unknown">Ocorreu um erro de autorização não identificado. Se o erro persistir, tenta fazer login através do navegador pelo menu.</string> + <string name="title_login">Entrar</string> + <string name="action_save">Guardar</string> + <string name="action_edit_profile">Editar perfil</string> + <string name="action_edit_own_profile">Editar</string> + <string name="action_undo">Desfazer</string> + <string name="action_accept">Aceitar</string> + <string name="action_reject">Rejeitar</string> + <string name="error_could_not_load_login_page">Não foi possível carregar a página de autenticação.</string> + <string name="error_loading_account_details">Erro ao carregar os detalhes da conta</string> + <string name="tips_push_notification_migration">Faz novamente login em todas as contas para ativar as notificações push.</string> + <string name="account_date_joined">Criada há %1$s</string> + <string name="dialog_push_notification_migration">Para ativar as notificações push através de UnifiedPush, o Tusky necessita de permissão para subscrever as notificações da tua instância Mastodon. Isto obriga a fazer login novamente, por forma a alterar o escopo das permissões fornecidas ao Tusky pelo OAuth. Usar a opção de novo login, aqui ou nas Configurações da Conta, preservará todos os teus rascunhos e cache locais.</string> + <string name="status_count_one_plus">+1</string> + <string name="dialog_push_notification_migration_other_accounts">Fizeste novo login na tua conta para dar permissão para a subscrição das notificações push no Tusky. Porém, ainda tens outras contas sem esta permissão. Faz novo login em cada uma das outras contas para ativares o suporte para notificações por UnifiedPush.</string> + <string name="action_edit_image">Editar imagem</string> + <string name="error_image_edit_failed">Não foi possível editar a imagem.</string> + <string name="saving_draft">A guardar rascunho…</string> + <string name="title_migration_relogin">Faça login novamente para ter notificações push</string> + <string name="action_dismiss">Descartar</string> + <string name="action_details">Detalhes</string> + <string name="delete_scheduled_post_warning">Apagar esta publicação agendada\?</string> + <string name="set_focus_description">Toca ou arrasta o círculo para escolher o ponto de focagem que estará sempre visível nas pré-visualizações.</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="duration_no_change">(Sem alteração)</string> + <string name="pref_show_self_username_always">Sempre</string> + <string name="pref_show_self_username_disambiguate">Quando autenticado em várias contas</string> + <string name="pref_show_self_username_never">Nunca</string> + <string name="description_post_language">Idioma da publicação</string> + <string name="pref_title_show_self_username">Mostrar o nome de utilizador nas barras de ferramentas</string> + <string name="error_multimedia_size_limit">Os ficheiros de áudio e vídeo não podem exceder os %1$s MB.</string> + <string name="action_set_focus">Define o ponto de focagem</string> + <string name="error_following_hashtag_format">Erro ao seguir #%1$s</string> + <string name="error_unfollowing_hashtag_format">Erro ao deixar de seguir #%1$s</string> + <string name="action_add_reaction">adicionar reação</string> + <string name="dialog_follow_hashtag_title">Seguir hashtag</string> + <string name="dialog_follow_hashtag_hint">#hashtag</string> + <string name="pref_summary_http_proxy_disabled">Desativada</string> + <string name="pref_summary_http_proxy_missing"><indefinido></string> + <string name="title_edits">Edições</string> + <string name="pref_title_notification_filter_reports">existe um novo relatório</string> + <string name="hint_media_description_missing">Imagem/Vídeo deve ter uma descrição.</string> + <string name="pref_title_http_proxy_port_message">Porta deve estar entre %1$d e %2$d</string> + <string name="action_post_failed">Erro no upload</string> + <string name="action_post_failed_detail">A tua publicação não conseguiu ser enviada e foi guardada nos rascunhos. +\n +\nO servidor não pode ser contactado ou rejeitou a publicação.</string> + <string name="action_post_failed_detail_plural">As tuas publicações não conseguiram ser enviadas e foram guardadas nos rascunhos. +\n +\nO servidor não pode ser contactado ou rejeitou as publicações.</string> + <string name="action_post_failed_show_drafts">Mostrar rascunhos</string> + <string name="action_post_failed_do_nothing">Descartar</string> + <string name="post_media_alt">ALT</string> + <string name="error_muting_hashtag_format">Erro ao silenciar #%1$s</string> + <string name="action_browser_login">Iniciar sessão com o navegador</string> + <string name="notification_header_report_format">%1$s reportou %2$s</string> + <string name="action_discard">Descartar alterações</string> + <string name="action_continue_edit">Continuar a editar</string> + <string name="action_share_account_link">Partilhar ligação para a conta</string> + <string name="action_share_account_username">Partilhar nome de utilizador da conta</string> + <string name="send_account_link_to">Partilhar ligação da conta para…</string> + <string name="send_account_username_to">Partilhar nome de utilizador da conta para…</string> + <string name="account_username_copied">Nome de utilizador copiado</string> + <string name="confirmation_hashtag_unfollowed">Deixou de seguir #%1$s</string> + <string name="post_edited">Editado a %1$s</string> + <string name="title_followed_hashtags">Hashtags seguidas</string> + <string name="title_public_trending_hashtags">Hashtags populares</string> + <string name="pref_reading_order_newest_first">Mais recentes primeiro</string> + <string name="pref_reading_order_oldest_first">Mais antigas primeiro</string> + <string name="action_refresh">Atualizar</string> + <string name="report_category_other">Outro</string> + <string name="report_category_spam">Spam</string> + <string name="title_public_trending_statuses">Publicações populares</string> + <string name="pref_ui_text_size">Tamanho do texto do interface</string> + <string name="label_image">Imagem</string> + <string name="pref_summary_http_proxy_invalid"><inválida></string> + <string name="notification_listenable_worker_description">Notificações quando Tusky está ativo em segundo plano</string> + <string name="notification_notification_worker">A carregar notificações…</string> + <string name="notification_listenable_worker_name">Atividade em segundo plano</string> + <string name="notification_prune_cache">Manutenção de cache…</string> + <string name="pref_default_post_language">Linguagem padrão da publicação</string> + <string name="notification_report_name">Denúncias</string> + <string name="notification_report_description">Notificações de denúncias</string> + <string name="about_device_info_title">O teu dispositivo</string> + <string name="notification_unknown_name">Desconhecido</string> + <string name="app_theme_system_black">Usar tema do Sistema (preto)</string> + <string name="error_following_hashtags_unsupported">Esta instância não suporta a funcionalidade que permite seguir hashtags.</string> + <string name="error_media_upload_sending_fmt">O envio falhou: %1$s</string> + <string name="pref_title_show_self_boosts">Mostrar boosts da própria conta</string> + <string name="pref_title_show_self_boosts_description">Alguém a dar boost à sua própria publicação</string> + <string name="about_device_info">%1$s %2$s +\nVersão Android: %3$s +\nVersão SDK: %4$d</string> + <string name="failed_to_remove_from_list">Erro ao remover a conta da lista</string> + <string name="action_add_or_remove_from_list">Adicionar ou remover da lista</string> + <string name="failed_to_add_to_list">Erro ao adicionar a conta à lista</string> + <string name="failed_to_pin">Erro ao Afixar</string> + <string name="compose_save_draft_loses_media">Guardar rascunho\? (Os anexos serão carregados novamente quando restaurares o rascunho.)</string> + <string name="select_list_manage">Gerir listas</string> + <string name="status_filter_placeholder_label_format">Filtrado: %1$s</string> + <string name="status_filtered_show_anyway">Mostrar à mesma</string> + <string name="description_post_edited">Editado</string> + <string name="about_account_info_title">A tua conta</string> + <string name="about_account_info">\@%1$s@%2$s +\nVersão: %3$s</string> + <string name="status_created_at_now">agora</string> + <string name="compose_unsaved_changes">Tens alterações por guardar.</string> + <string name="pref_title_account_filter_keywords">Perfis</string> + <string name="compose_delete_draft">Apagar rascunho\?</string> + <string name="post_media_image">Imagem</string> + <string name="a11y_label_loading_thread">A carregar conversa</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="instance_rule_info">Ao fazer login, concordas com as regras de %1$s.</string> + <string name="instance_rule_title">%1$s regras</string> + <string name="ui_error_unknown">razão desconhecida</string> + <string name="mute_notifications_switch">Silenciar notificações</string> + <string name="accessibility_talking_about_tag">%1$d pessoas estão a falar sobre a hashtag %2$s</string> + <string name="total_accounts">Total de contas</string> + <string name="status_edit_info">Editada: %1$s</string> + <string name="status_created_info">Criada: %1$s</string> + <string name="pref_title_reading_order">Ordem de leitura</string> + <string name="report_category_violation">Violação de uma regra</string> + <string name="no_lists">Não tens nenhuma lista.</string> + <string name="socket_timeout_exception">A tentativa de contacto com o teu servidor demorou muito tempo</string> + <string name="action_unfollow_hashtag_format">Deixar de seguir #%1$s\?</string> + <string name="pref_title_show_stat_inline">Mostrar estatísticas das publicações na timeline</string> + <string name="action_view_filter">Ver filtro</string> + <string name="error_status_source_load">Falha ao carregar a origem de estado do servidor.</string> + <string name="failed_to_unpin">Erro ao remover dos afixados</string> + <string name="about_copy">Copiar informação da versão e do equipamento</string> + <string name="about_copied">Informação da versão e do equipamento copiada</string> + <string name="filter_edit_keyword_title">Editar palavra-chave</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="list_exclusive_label">Esconder da timeline inicial</string> + <string name="error_media_playback">Reprodução falhada: %1$s</string> + <string name="dialog_delete_filter_positive_action">Apagar</string> + <string name="ui_error_clear_notifications">Erro ao limpar as notificações: %1$s</string> + <string name="ui_error_vote">Erro ao votar no questionário: %1$s</string> + <string name="ui_error_accept_follow_request">Erro ao aceitar o pedido para te seguir: %1$s</string> + <string name="ui_error_favourite">Erro ao adicionar a publicação aos favoritos: %1$s</string> + <string name="ui_error_reblog">Erro ao dar boost à publicação: %1$s</string> + <string name="dialog_delete_filter_text">Apagar filtro \'%1$s\'\?</string> + <string name="error_unmuting_hashtag_format">Erro a remover silêncio de #%1$s</string> + <string name="description_login">Funciona na maioria dos casos. Nenhuma informação é partilhada com outras aplicações.</string> + <string name="description_browser_login">Poderá permitir métodos de autenticação adicionais, mas precisará de um navegador suportado.</string> + <string name="filter_description_hide">Esconder completamente</string> + <string name="notification_summary_report_format">%1$s · %2$d publicações anexados</string> + <string name="notification_report_format">Nova denúncia de %1$s</string> + <string name="filter_keyword_addition_title">Adicionar palavra-chave</string> + <string name="unfollowing_hashtag_success_format">Já não segues a hashtag #%1$s</string> + <string name="dialog_save_profile_changes_message">Queres guardar as alterações ao teu perfil\?</string> + <string name="action_add">Adcionar</string> + <string name="ui_error_bookmark">Erro ao guardar a publicação: %1$s</string> + <string name="hint_filter_title">O meu filtro</string> + <string name="label_filter_title">Título</string> + <string name="filter_action_warn">Avisar</string> + <string name="filter_action_hide">Esconder</string> + <string name="filter_description_warn">Esconder com um aviso</string> + <string name="error_blocking_domain">Falha ao silenciar %1$s: %2$s</string> + <string name="error_unblocking_domain">Falha ao remover o silêncio de %1$s: %2$s</string> + <string name="following_hashtag_success_format">Agora segues a hashtag #%1$s</string> + <string name="load_newest_notifications">Carregar as notificações mais recentes</string> + <string name="error_missing_edits">O teu servidor sabe que esta publicação foi editada, mas não tem uma cópia da edição, portanto não te consegue mostrar. +\n +\nIsto é <a href="https://github.com/mastodon/mastodon/issues/25398"> o problema com Mastodon #25398</a>.</string> + <string name="list_reply_policy_none">Ninguém</string> + <string name="list_reply_policy_list">Membros da lista</string> + <string name="list_reply_policy_followed">Qualquer utilizador que segues</string> + <string name="list_reply_policy_label">Mostrar respostas a</string> + <string name="total_usage">Uso total</string> + <string name="ui_error_reject_follow_request">Rejeição de seguidor pendente falhou: %1$s</string> + <string name="label_filter_keywords">Palavras-chave ou frases para filtrar</string> + <string name="filter_keyword_display_format">%1$s (palavra completa)</string> + <string name="ui_success_accepted_follow_request">Seguidor pendente aceite</string> + <string name="ui_success_rejected_follow_request">Seguidor pendente bloqueado</string> + <string name="label_filter_action">Ação de filtro</string> + <string name="help_empty_conversations">As tuas <b>mensagens privadas</b> estão aqui, também chamadas de conversas ou mensagens diretas (MD/DM). +\n +\nAs mensagens privadas são criadas definindo a visibilidade [iconics gmd_public] de uma publicação para [iconics gmd_mail] <i>Direta</i> e mencionando um ou mais utilizadores no texto. +\n +\nPor exemplo, podes entrar na vista de perfil de uma conta e pressionar o botão de criar publicação [iconics gmd_edit] e alterar a visibilidade. </string> + <string name="help_empty_lists">Esta é a tua <b>vista de listas</b>. Podes criar várias listas privadas e adicionar contas nelas. +\n +\nAtenção, apenas podes adicionar contas que segues às tuas listas. +\n +\nEstas listas podem ser usadas como separadores nas Configurações da Conta [iconics gmd_account_circle] [iconics gmd_navigate_next] Separadores. </string> + <string name="help_empty_home">Esta é a tua <b>timeline inicial</b>. Ela mostra as publicações mais recentes das contas que segues. +\n +\nPara explorar contas, podes descobri-las numa das outras timelines. Por exemplo, a timeline local da tua instância [iconics gmd_group]. Ou podes pesquisá-las pelo nome [iconics gmd_search]; por exemplo, pesquisando por Tusky para encontrar a nossa conta Mastodon.</string> + <string name="unmuting_hashtag_success_format">Remover silêncio da hashtag #%1$s</string> + <string name="muting_hashtag_success_format">Silenciar hashtag #%1$s como um aviso</string> + <string name="pref_title_per_timeline_preferences">Configurações por timeline</string> + <string name="label_filter_context">Contextos de filtro</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml new file mode 100644 index 0000000..44ae9e0 --- /dev/null +++ b/app/src/main/res/values-ru/strings.xml @@ -0,0 +1,723 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources xmlns:tools="http://schemas.android.com/tools"> + <string name="error_generic">Возникла ошибка.</string> + <string name="error_network">Произошла ошибка сети. Пожалуйста, проверьте интернет-соединение и попробуйте снова.</string> + <string name="error_empty">Не может быть пустым.</string> + <string name="error_invalid_domain">Введен недопустимый домен</string> + <string name="error_failed_app_registration">Не удалось пройти аутентификацию в этом экземпляре. Если ситуация не изменилась, попробуйте войти в систему через браузер из меню.</string> + <string name="error_no_web_browser_found">Не удалось найти веб-браузер для использования.</string> + <string name="error_authorization_unknown">Произошла неопознанная ошибка авторизации. Если ошибка не исчезла, попробуйте войти в систему через браузер из меню.</string> + <string name="error_authorization_denied">В авторизации было отказано. Если вы уверены, что ввели правильные учетные данные, попробуйте войти из браузера в меню.</string> + <string name="error_retrieving_oauth_token">Не удалось получить токен входа в систему. Если проблема сохраняется, попробуйте войти в систему через браузер из меню.</string> + <string name="error_compose_character_limit">Пост слишком длинный!</string> + <string name="error_media_upload_type">Этот тип файла не может быть загружен.</string> + <string name="error_media_upload_opening">Этот файл не удалось открыть.</string> + <string name="error_media_upload_permission">Требуется разрешение на чтение медиа.</string> + <string name="error_media_download_permission">Необходимо разрешение на хранение медиа.</string> + <string name="error_media_upload_image_or_video">Изображения и видео не могут быть прикреплены к одному и тому же посту.</string> + <string name="error_media_upload_sending">Загрузка не удалась.</string> + <string name="error_sender_account_gone">Ошибка при отправке сообщения.</string> + <string name="title_home">Главная</string> + <string name="title_notifications">Уведомления</string> + <string name="title_public_local">Местная лента</string> + <string name="title_public_federated">Объединенная лента</string> + <string name="title_direct_messages">Личные сообщения</string> + <string name="title_tab_preferences">Вкладки</string> + <string name="title_view_thread">Обсуждение</string> + <string name="title_posts">Посты</string> + <string name="title_posts_with_replies">С ответами</string> + <string name="title_posts_pinned">Закрепленные</string> + <string name="title_follows">Подписки</string> + <string name="title_followers">Подписчики</string> + <string name="title_favourites">Избранное</string> + <string name="title_mutes">Заглушенные пользователи</string> + <string name="title_blocks">Заблокированные пользователи</string> + <string name="title_follow_requests">Запросы на подписку</string> + <string name="title_edit_profile">Редактировать профиль</string> + <string name="title_drafts">Черновики</string> + <string name="title_licenses">Лицензии</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s поделился(-ась)</string> + <string name="post_sensitive_media_title">Содержание, которое может быть неприятным или нежелательным</string> + <string name="post_media_hidden_title">Медиа скрыто</string> + <string name="post_sensitive_media_directions">Нажмите для просмотра</string> + <string name="post_content_warning_show_more">Еще</string> + <string name="post_content_warning_show_less">Свернуть</string> + <string name="post_content_show_more">Раскрыть</string> + <string name="post_content_show_less">Свернуть</string> + <string name="message_empty">Тут ничего нет.</string> + <string name="footer_empty">Тут ничего нет. Потяните вниз, чтобы обновить!</string> + <string name="notification_reblog_format">%1$s поделился(-ась) вашей записью</string> + <string name="notification_favourite_format">%1$s отдал(а) предпочтение вашему посту</string> + <string name="notification_follow_format">%1$s подписался(-лась) на вас</string> + <string name="report_username_format">Пожаловаться на @%1$s</string> + <string name="report_comment_hint">Дополнительные замечания\?</string> + <string name="action_quick_reply">Быстрый ответ</string> + <string name="action_reply">Ответить</string> + <string name="action_reblog">Поделиться</string> + <string name="action_unreblog">Убрать цитирование</string> + <string name="action_favourite">Избрать</string> + <string name="action_unfavourite">Убрать из избранного</string> + <string name="action_more">Больше</string> + <string name="action_compose">Написать</string> + <string name="action_login">Вход с помощью Tusky</string> + <string name="action_logout">Выйти</string> + <string name="action_logout_confirm">Вы уверены, что хотите выйти из %1$s\? Это приведет к удалению всех локальных данных учетной записи, включая черновики и настройки.</string> + <string name="action_follow">Подписаться</string> + <string name="action_unfollow">Отписаться</string> + <string name="action_block">Заблокировать</string> + <string name="action_unblock">Разблокировать</string> + <string name="action_hide_reblogs">Скрыть цитирование</string> + <string name="action_show_reblogs">Показать цитирование</string> + <string name="action_report">Пожаловаться</string> + <string name="action_delete">Удалить</string> + <string name="action_delete_and_redraft">Удалить и пересоздать</string> + <string name="action_send">ОТПРАВИТЬ</string> + <string name="action_send_public">ЗАПОСТИТЬ!</string> + <string name="action_retry">Повторить</string> + <string name="action_close">Закрыть</string> + <string name="action_view_profile">Профиль</string> + <string name="action_view_preferences">Настройки</string> + <string name="action_view_account_preferences">Настройки учетной записи</string> + <string name="action_view_favourites">Избранное</string> + <string name="action_view_mutes">Заглушенные пользователи</string> + <string name="action_view_blocks">Заблокированные пользователи</string> + <string name="action_view_follow_requests">Запросы на подписку</string> + <string name="action_view_media">Медиа</string> + <string name="action_open_in_web">Открыть в браузере</string> + <string name="action_add_media">Добавить медиа</string> + <string name="action_photo_take">Сделать снимок</string> + <string name="action_share">Поделиться</string> + <string name="action_mute">Заглушить</string> + <string name="action_unmute">Не глушить</string> + <string name="action_mention">Упомянуть</string> + <string name="action_hide_media">Скрыть медиа</string> + <string name="action_open_drawer">Открыть всплывающее меню</string> + <string name="action_save">Сохранить</string> + <string name="action_edit_profile">Редактировать профиль</string> + <string name="action_edit_own_profile">Изменить</string> + <string name="action_undo">Отменить</string> + <string name="action_accept">Принять</string> + <string name="action_reject">Отклонить</string> + <string name="action_search">Поиск</string> + <string name="action_access_drafts">Черновики</string> + <string name="action_toggle_visibility">Видимость поста</string> + <string name="action_content_warning">Предупреждения о нежелательном содержимом</string> + <string name="action_emoji_keyboard">Эмодзи-клавиатура</string> + <string name="action_add_tab">Добавить вкладку</string> + <string name="action_links">Ссылки</string> + <string name="action_mentions">Упоминания</string> + <string name="action_hashtags">Хэштеги</string> + <string name="action_open_reblogger">Перейти к автору цитирования</string> + <string name="action_open_reblogged_by">Показать цитирования</string> + <string name="action_open_faved_by">Показать избранное</string> + <string name="title_hashtags_dialog">Хэштеги</string> + <string name="title_mentions_dialog">Упоминания</string> + <string name="title_links_dialog">Ссылки</string> + <string name="action_open_media_n">Открыть медиа #%1$d</string> + <string name="download_image">Загружаем %1$s</string> + <string name="action_copy_link">Копировать ссылку</string> + <string name="action_open_as">Открыть как %1$s</string> + <string name="action_share_as">Поделиться как…</string> + <string name="download_media">Скачать медиа</string> + <string name="downloading_media">Скачивание медиа</string> + <string name="send_post_link_to">Поделиться URL-ссылкой на запись…</string> + <string name="send_post_content_to">Поделится постом с…</string> + <string name="send_media_to">Поделится медиа в…</string> + <string name="confirmation_reported">Отправлено!</string> + <string name="confirmation_unblocked">Пользователь разблокирован</string> + <string name="confirmation_unmuted">Глушение пользователя снято</string> + <string name="hint_domain">Какой экземпляр\?</string> + <string name="hint_compose">Что случилось\?</string> + <string name="hint_content_warning">Предупреждение о потенциально нежелательном содержании</string> + <string name="hint_display_name">Отображаемое имя</string> + <string name="hint_note">О себе</string> + <string name="hint_search">Поиск…</string> + <string name="search_no_results">Без результатов</string> + <string name="label_quick_reply">Ответить…</string> + <string name="label_avatar">Аватар</string> + <string name="label_header">Заголовок</string> + <string name="link_whats_an_instance">Что такое экземпляр\?</string> + <string name="login_connection">Подключаюсь…</string> + <string name="dialog_whats_an_instance">Здесь можно указать адрес или домен любого экземпляра, например mastodon.social, icosahedron.website, social.tchncs.de и <a href="https://instances.social">другие!</a>. +\n +\nЕсли у вас еще нет учетной записи Вы можете ввести имя экземпляра к которому хотите присоединиться и создать там учетную запись. +\n +\nЭкземпляр - это отдельное место где размещен ваш аккаунт, но вы можете легко общаться с людьми на других экземплярах и следить за ними как будто вы находитесь на том же сайте. +\n +\nБолее подробную информацию можно найти на сайте <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="dialog_title_finishing_media_upload">Завершение загрузки медиа</string> + <string name="dialog_message_uploading_media">Загружаем…</string> + <string name="dialog_download_image">Загрузить</string> + <string name="dialog_message_cancel_follow_request">Отозвать запрос на подписку\?</string> + <string name="dialog_unfollow_warning">Отписаться от этого аккаунта?</string> + <string name="dialog_delete_post_warning">Удалить пост\?</string> + <string name="dialog_redraft_post_warning">Удалить и переписать этот пост\?</string> + <string name="visibility_public">Публично: Написать в общедоступных лентах</string> + <string name="visibility_unlisted">Скрытно: Не показывать в общедоступных лентах</string> + <string name="visibility_private">Только подписчики: Публикуйте записи только для подписчиков</string> + <string name="visibility_direct">Напрямую: сделать запись только для упомянутых пользователей</string> + <string name="pref_title_edit_notification_settings">Уведомления</string> + <string name="pref_title_notifications_enabled">Уведомления</string> + <string name="pref_title_notification_alerts">Оповещения</string> + <string name="pref_title_notification_alert_sound">Уведомить со звуком</string> + <string name="pref_title_notification_alert_vibrate">Уведомить с вибрацией</string> + <string name="pref_title_notification_alert_light">Уведомить со светом</string> + <string name="pref_title_notification_filters">Уведомлять меня если</string> + <string name="pref_title_notification_filter_mentions">меня упомянули</string> + <string name="pref_title_notification_filter_follows">кто-то подписался</string> + <string name="pref_title_notification_filter_reblogs">мои посты процитировали</string> + <string name="pref_title_notification_filter_favourites">мой пост был избран</string> + <string name="pref_title_appearance_settings">Внешний вид</string> + <string name="pref_title_app_theme">Тема приложения</string> + <string name="pref_title_timelines">Хроники</string> + <string name="pref_title_timeline_filters">Фильтры</string> + <string name="app_them_dark">Тёмная</string> + <string name="app_theme_light">Светлая</string> + <string name="app_theme_black">Чёрная</string> + <string name="app_theme_auto">Автоматически на закате</string> + <string name="app_theme_system">Как в системе</string> + <string name="pref_title_browser_settings">Браузер</string> + <string name="pref_title_custom_tabs">Использовать пользовательские вкладки Chrome</string> + <string name="pref_title_language">Язык</string> + <string name="pref_title_post_filter">Фильтрование ленты</string> + <string name="pref_title_post_tabs">Bкладки</string> + <string name="pref_title_show_boosts">Показать цитирования</string> + <string name="pref_title_show_replies">Показать ответы</string> + <string name="pref_title_show_media_preview">Загрузить предпросмотр медиа</string> + <string name="pref_title_proxy_settings">Прокси</string> + <string name="pref_title_http_proxy_settings">HTTP прокси</string> + <string name="pref_title_http_proxy_enable">Включить HTTP прокси</string> + <string name="pref_title_http_proxy_server">Адрес HTTP прокси сервера</string> + <string name="pref_title_http_proxy_port">Порт</string> + <string name="pref_default_post_privacy">Конфиденциальность постов по умолчанию</string> + <string name="pref_default_media_sensitivity">Всегда помечать медиа как деликатные</string> + <string name="pref_publishing">Опубликовывать (синхронизировано с сервером)</string> + <string name="pref_failed_to_sync">Не удалось синхронизировать настройки</string> + <string name="post_privacy_public">Публично</string> + <string name="post_privacy_unlisted">Скрытно</string> + <string name="post_privacy_followers_only">Только для подписчиков</string> + <string name="pref_post_text_size">Размер текста сообщения</string> + <string name="post_text_size_smallest">Крохотный</string> + <string name="post_text_size_small">Маленький</string> + <string name="post_text_size_medium">Средний</string> + <string name="post_text_size_large">Большой</string> + <string name="post_text_size_largest">Огромный</string> + <string name="notification_mention_name">Новые упоминания</string> + <string name="notification_mention_descriptions">Уведомлять о новых упоминаниях</string> + <string name="notification_follow_name">Новые подписчики</string> + <string name="notification_follow_description">Уведомлять о новых подписчиках</string> + <string name="notification_boost_name">Цитирования</string> + <string name="notification_boost_description">Уведомлять, если кто-то процитировал ваши записи</string> + <string name="notification_favourite_name">Избранное</string> + <string name="notification_favourite_description">Уведомлять, когда ваши сообщения добавляют в «избранное»</string> + <string name="notification_poll_name">Опросы</string> + <string name="notification_poll_description">Уведомлять о завершившихся опросах</string> + <string name="notification_mention_format">%1$s упомянул(а) вас</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s и %4$d других</string> + <string name="notification_summary_medium">%1$s, %2$s, и %3$s</string> + <string name="notification_summary_small">%1$s и %2$s</string> + <plurals name="notification_title_summary"> + <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="description_account_locked">Закрытый аккаунт</string> + <string name="about_title_activity">О приложении</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tusky — это свободное программное обеспечение с открытым исходным кодом. Оно лицензировано в соответствии с GNU General Public License Version 3. Ознакомиться с лицензией можно здесь: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: the url can be changed to link to the localized version of the license --> + <string name="about_project_site">Веб-сайт проекта: https://tusky.app</string> + <string name="about_bug_feature_request_site">Отчеты об ошибках и ваши пожелания: +\n https://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Профиль Tusky</string> + <string name="post_share_content">Поделиться содержанием поста</string> + <string name="post_share_link">Поделиться ссылкой на запись</string> + <string name="post_media_images">Изображения</string> + <string name="post_media_video">Видео</string> + <string name="state_follow_requested">Запрос на подписку отправлено</string> + <!--Отметки времени у постов: "16s" or "2d"--> + <string name="abbreviated_in_years">через %1$dг</string> + <string name="abbreviated_in_days">через %1$dд</string> + <string name="abbreviated_in_hours">через %1$dч</string> + <string name="abbreviated_in_minutes">через %1$dм</string> + <string name="abbreviated_in_seconds">через %1$dс</string> + <string name="abbreviated_years_ago">%1$dг</string> + <string name="abbreviated_days_ago">%1$dд</string> + <string name="abbreviated_hours_ago">%1$dч</string> + <string name="abbreviated_minutes_ago">%1$dм</string> + <string name="abbreviated_seconds_ago">%1$dс</string> + <!--Оставшееся время в опросах --> + <plurals name="poll_timespan_days"> + <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> + <plurals name="poll_timespan_hours"> + <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> + <plurals name="poll_timespan_minutes"> + <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> + <plurals name="poll_timespan_seconds"> + <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="follows_you">Подписан(а) на вас</string> + <string name="pref_title_alway_show_sensitive_media">Всегда показывать потенциально нежелательный контент</string> + <string name="title_media">Медиа</string> + <string name="replying_to">Ответить @%1$s</string> + <string name="load_more_placeholder_text">загрузить ещё</string> + <string name="pref_title_public_filter_keywords">Публичные ленты</string> + <string name="pref_title_thread_filter_keywords">Разговоры</string> + <string name="filter_addition_title">Добавить фильтр</string> + <string name="filter_edit_title">Изм. фильтр</string> + <string name="filter_dialog_remove_button">Удалить</string> + <string name="filter_dialog_update_button">Обновить</string> + <string name="filter_add_description">Фраза для фильтрации</string> + <string name="add_account_name">Добавить аккаунт</string> + <string name="add_account_description">Добавить новую учетную запись Mastodon</string> + <string name="action_lists">Списки</string> + <string name="title_lists">Списки</string> + <string name="error_create_list">Не удалось создать список</string> + <string name="error_rename_list">Не удалось обновить список</string> + <string name="error_delete_list">Не удалось удалить список</string> + <string name="action_create_list">Создать список</string> + <string name="action_rename_list">Обновить список</string> + <string name="action_delete_list">Удалить список</string> + <string name="hint_search_people_list">Поиск среди людей на которых вы подписаны</string> + <string name="action_add_to_list">Добавить аккаунт в список</string> + <string name="action_remove_from_list">Удалить аккаунт из списка</string> + <string name="compose_active_account_description">Отправить как %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <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="action_set_caption">Добавить подпись</string> + <string name="action_remove">Удалить</string> + <string name="lock_account_label">Закрыть аккаунт</string> + <string name="lock_account_label_description">Вам придётся вручную подтверждать подписчиков</string> + <string name="compose_save_draft">Сохранить черновик?</string> + <string name="send_post_notification_title">Отправляем пост…</string> + <string name="send_post_notification_error_title">Ошибка при отправке поста</string> + <string name="send_post_notification_channel_name">Отправка поста</string> + <string name="send_post_notification_cancel_title">Отправка отменена</string> + <string name="send_post_notification_saved_content">Копия этого сообщения сохранена в ваших черновиках</string> + <string name="action_compose_shortcut">Написать</string> + <string name="error_no_custom_emojis">У вашего экземпляра %1$s нет собственных эмодзи</string> + <string name="emoji_style">Стиль эмодзи</string> + <string name="system_default">Задан системой по умолчанию</string> + <string name="download_fonts">Эти наборы эмодзи сперва нужно загрузить</string> + <string name="performing_lookup_title">Выполняем поиск…</string> + <string name="expand_collapse_all_posts">Развернуть/свернуть все посты</string> + <string name="action_open_post">Открыть пост</string> + <string name="restart_required">Требуется перезапуск приложения</string> + <string name="restart_emoji">Вам нужно будет перезапустить Tusky, чтобы применить эти изменения.</string> + <string name="later">Позже</string> + <string name="restart">Перезапустить</string> + <string name="caption_systememoji">Набор эмодзи по умолчанию на этом устройстве</string> + <string name="caption_blobmoji" tools:ignore="TypographyDashes">Набор эмодзи Blob из Android 4.4-7.1</string> + <string name="caption_twemoji">Стандартный набор эмодзи в Mastodon</string> + <string name="download_failed">Загрузка не удалась</string> + <string name="profile_badge_bot_text">Бот</string> + <string name="account_moved_description">%1$s переехал(а) на:</string> + <string name="reblog_private">Процитировать для изначальной аудитории</string> + <string name="unreblog_private">Не цитировать</string> + <string name="license_description">Tusky использует код и материалы из следующих проектов с открытым исходным кодом:</string> + <string name="license_apache_2">Используется лицензия Apache License (копия ниже)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Метаданные профиля</string> + <string name="profile_metadata_add">добавить данные</string> + <string name="profile_metadata_label_label">Этикетка</string> + <string name="profile_metadata_content_label">Содержание</string> + <string name="pref_title_absolute_time">Показывать абсолютное время</string> + <string name="label_remote_account">Информация ниже может отображать профиль пользователя не полностью. Нажмите чтобы открыть полный профиль в браузере.</string> + <string name="unpin_action">Открепить</string> + <string name="pin_action">Прикрепить</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Понравился</item> + <item quantity="few"><b>%1$s</b> Понравилось</item> + <item quantity="many"><b>%1$s</b> Понравилось</item> + <item quantity="other"><b>%1$s</b> Понравилось</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> Процитировал(а)</item> + <item quantity="few"><b>%1$s</b> Процитировали</item> + <item quantity="many"><b>%1$s</b> Процитировали</item> + <item quantity="other"><b>%1$s</b> Процитировали</item> + </plurals> + <string name="title_reblogged_by">Процитировано</string> + <string name="title_favourited_by">Понравилось</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s и %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s и ещё %3$d</string> + <string name="description_post_media">Медиа: %1$s</string> + <string name="description_post_cw">Предупреждение о потенциально нежелательном материале %1$s</string> + <string name="description_post_media_no_description_placeholder">Без описания</string> + <string name="description_post_reblogged">Распространено</string> + <string name="description_post_favourited">Понравилось</string> + <string name="description_visibility_public">Публично</string> + <string name="description_visibility_unlisted">Скрыто</string> + <string name="description_visibility_private">Подписчики</string> + <string name="description_visibility_direct">Прямо</string> + <string name="hint_list_name">Название списка</string> + <string name="edit_hashtag_hint">Хэштег без #</string> + <string name="notifications_clear">Очистить</string> + <string name="notifications_apply_filter">Фильтр</string> + <string name="filter_apply">Применить</string> + <string name="compose_shortcut_long_label">Создать пост</string> + <string name="compose_shortcut_short_label">Написать</string> + <string name="pref_title_bot_overlay">Показать индикаторы для ботов</string> + <string name="notification_clear_text">Вы точно хотите удалить навсегда все ваши уведомления\?</string> + <string name="poll_info_format"> <!-- 15 голосов • 1 час остался--> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s голос</item> + <item quantity="few">%1$s голоса</item> + <item quantity="many">%1$s голосов</item> + <item quantity="other">%1$s голосов</item> + </plurals> + <string name="poll_info_time_absolute">завершится %1$s</string> + <string name="poll_info_closed">закрыт</string> + <string name="poll_vote">Голосовать</string> + <string name="poll_ended_voted">Голосование в котором вы принимали участие завершилось</string> + <string name="poll_ended_created">Созданный вами опрос завершился</string> + <string name="button_continue">Продолжить</string> + <string name="button_back">Назад</string> + <string name="button_done">Готово</string> + <string name="hint_additional_info">Дополнительные замечания</string> + <string name="title_domain_mutes">Скрытые домены</string> + <string name="action_view_domain_mutes">Скрытые домены</string> + <string name="action_mute_domain">Заглушить %1$s</string> + <string name="confirmation_domain_unmuted">%1$s не скрыт</string> + <string name="mute_domain_warning">Вы уверены, что хотите заблокировать все %1$s\? Вы не увидите контент с этого домена ни в публичных лентах, ни в своих уведомлениях. Ваши подписчики с этого домена будут удалены.</string> + <string name="mute_domain_warning_dialog_ok">Скрыть весь домен</string> + <string name="pref_title_notification_filter_poll">опросы закончились</string> + <string name="pref_title_animate_gif_avatars">Анимировать GIF-аватары</string> + <string name="filter_dialog_whole_word">Слово целиком</string> + <string name="filter_dialog_whole_word_description">Если ключевое слово или фраза состоит только из букв или цифр, они будут учитываться только при полном совпадении</string> + <string name="caption_notoemoji">Текущий набор эмодзи от Google</string> + <string name="description_poll">Голосование с вариантами: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="compose_preview_image_description">Действия для изображения %1$s</string> + <string name="report_sent_success">Жалоба @%1$s успешно отправлена</string> + <string name="report_remote_instance">Переслать в %1$s</string> + <string name="failed_report">Не удалось пожаловаться</string> + <string name="failed_fetch_posts">Не удалось получить посты</string> + <string name="report_description_1">Жалоба будет отправлена модератору вашего экземпляра. Ниже вы можете дать объяснение о причинах жалобы на эту учетную запись:</string> + <string name="report_description_remote_instance">Этот аккаунт расположен на другом экземпляре. Отправить анонимную копию жалобы туда тоже\?</string> + <string name="action_add_poll">Добавить опрос</string> + <string name="pref_title_alway_open_spoiler">Всегда разворачивать посты с предупреждением о нежелательном контенте</string> + <string name="title_accounts">Аккаунты</string> + <string name="failed_search">Поиск завершился ошибкой</string> + <string name="create_poll_title">Опрос</string> + <string name="duration_5_min">5 минут</string> + <string name="duration_30_min">30 минут</string> + <string name="duration_1_hour">1 час</string> + <string name="duration_6_hours">6 часов</string> + <string name="duration_1_day">1 день</string> + <string name="duration_3_days">3 дня</string> + <string name="duration_7_days">7 дней</string> + <string name="add_poll_choice">Добавить вариант</string> + <string name="poll_allow_multiple_choices">Несколько вариантов</string> + <string name="poll_new_choice_hint">Вариант %1$d</string> + <string name="edit_poll">Изменить</string> + <string name="title_scheduled_posts">Запланированные посты</string> + <string name="action_edit">Править</string> + <string name="action_access_scheduled_posts">Запланированные посты</string> + <string name="action_schedule_post">Запланировать пост</string> + <string name="action_reset_schedule">Сброс</string> + <string name="title_bookmarks">Закладки</string> + <string name="action_bookmark">Добавить в закладки</string> + <string name="action_view_bookmarks">Закладки</string> + <string name="about_powered_by_tusky">Работает на Tusky</string> + <string name="description_post_bookmarked">Добавлено в закладки</string> + <string name="select_list_title">Выбрать список</string> + <string name="list">Список</string> + <string name="post_lookup_error_format">Ошибка при поиске поста %1$s</string> + <string name="no_drafts">У вас нет никаких черновиков.</string> + <string name="no_scheduled_posts">У вас нет запланированных постов.</string> + <string name="warning_scheduling_interval">Минимальный интервал планирования в Mastodon составляет 5 минут.</string> + <string name="pref_title_confirm_reblogs">Запрашивать подтверждение перед цитированием</string> + <string name="pref_title_show_cards_in_timelines">Показывать предпросмотр ссылок в ленте</string> + <string name="pref_title_enable_swipe_for_tabs">Включите жест пролистывания для переключения между вкладками</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s человек</item> + <item quantity="few">%1$s людей</item> + <item quantity="many">%1$s человек</item> + <item quantity="other">%1$s человек</item> + </plurals> + <string name="notification_follow_request_description">Уведомления о запросах на подписку</string> + <string name="notification_follow_request_name">Запросы на подписку</string> + <string name="pref_title_notification_filter_follow_requests">получен запрос на подписку</string> + <string name="dialog_mute_warning">Заглушить @%1$s\?</string> + <string name="dialog_block_warning">Заблокировать @%1$s\?</string> + <string name="action_unmute_conversation">Не глушить обсуждение</string> + <string name="action_mute_conversation">Заглушить обсуждение</string> + <string name="notification_follow_request_format">отправлен запрос на подписку от %1$s</string> + <string name="hashtags">Хэштеги</string> + <string name="add_hashtag_title">Добавить хэштэг</string> + <string name="pref_title_gradient_for_media">Отображать цветные градиенты вместо скрытых медиа</string> + <string name="pref_main_nav_position_option_bottom">Снизу</string> + <string name="pref_main_nav_position_option_top">Сверху</string> + <string name="pref_main_nav_position">Расположение панели навигации</string> + <string name="action_unmute_domain">Не глушить %1$s</string> + <string name="dialog_mute_hide_notifications">Скрыть уведомления</string> + <string name="action_unmute_desc">Не глушить %1$s</string> + <string name="account_note_saved">Сохранено!</string> + <string name="account_note_hint">Ваша личная заметка об этой учётной записи</string> + <string name="pref_title_hide_top_toolbar">Скрыть заголовок верхней панели инструментов</string> + <string name="no_announcements">Объявлений нет.</string> + <string name="title_announcements">Объявления</string> + <string name="wellbeing_mode_notice">Некоторая информация, которая может повлиять на ваш комфорт будет скрыта. Она включает: +\n +\n - Уведомления о добавлении в избранное/цитировании/подписке +\n - Количество добавлений в избранное/количество цитирований +\n - Статистика подписчиков/постов в профилях +\n +\n На всплывающие уведомления это не повлияет, однако, вы можете пересмотреть настройки уведомлений самостоятельно.</string> + <string name="pref_title_wellbeing_mode">Комфорт</string> + <string name="duration_indefinite">Без ограничений</string> + <string name="label_duration">Продолжительность</string> + <string name="post_media_attachments">Вложения</string> + <string name="post_media_audio">Аудио</string> + <string name="notification_subscription_format">%1$s только что опубликовал(а)</string> + <string name="follow_requests_info">Несмотря на то, что ваш аккаунт не закрыт, сотрудники %1$s решили, что вы захотите подтверждать запросы на подписку от этих аккаунтов вручную.</string> + <plurals name="error_upload_max_media_reached"> + <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="wellbeing_hide_stats_posts">Скрыть количественную статистику постов</string> + <string name="action_unsubscribe_account">Отписаться</string> + <string name="action_subscribe_account">Подписаться</string> + <string name="drafts_post_reply_removed">Пост который Вы записали в черновик был удален</string> + <string name="drafts_failed_loading_reply">Не удалось загрузить информацию об ответах</string> + <string name="draft_deleted">Черновик удалён</string> + <string name="drafts_post_failed_to_send">Этот пост не удалось отправить!</string> + <string name="dialog_delete_list_warning">Вы действительно хотите удалить список %1$s\?</string> + <string name="wellbeing_hide_stats_profile">Скрыть количественную статистику профилей</string> + <string name="limit_notifications">Ограничить уведомления в ленте</string> + <string name="review_notifications">Просмотр уведомлений</string> + <string name="notification_subscription_description">Уведомлять о новых постах пользователей на которых Вы подписаны</string> + <string name="notification_subscription_name">Новые посты</string> + <string name="pref_title_animate_custom_emojis">Анимировать собственные эмодзи</string> + <string name="pref_title_notification_filter_subscriptions">кое-кто, на кого я подписан, опубликовал новое сообщение</string> + <string name="dialog_delete_conversation_warning">Удалить этот разговор\?</string> + <string name="action_delete_conversation">Удалить разговор</string> + <string name="pref_title_confirm_favourites">Запрашивать подтверждение перед добавлением в избранное</string> + <string name="action_unbookmark">Убрать из закладок</string> + <string name="a11y_label_loading_thread">Загрузка обсуждения</string> + <string name="pref_reading_order_newest_first">Сначала новые</string> + <string name="title_edits">Правки</string> + <string name="status_edit_info">%1$s отредактировал</string> + <string name="status_created_info">%1$s создал</string> + <string name="title_login">Войти</string> + <string name="action_browser_login">Вход через Браузер</string> + <string name="error_image_edit_failed">Изображение не удалось отредактировать.</string> + <string name="error_multimedia_size_limit">Размер видео и аудио не может превышать %1$s МБ.</string> + <string name="error_muting_hashtag_format">Не удалось заглушить #%1$s</string> + <string name="error_following_hashtags_unsupported">Этот экземпляр не поддерживает подписку на хештеги.</string> + <string name="error_following_hashtag_format">Не удалось подписаться на #%1$s</string> + <string name="error_unfollowing_hashtag_format">Не удалось отписаться от #%1$s</string> + <string name="error_media_upload_sending_fmt">Загрузка не удалась: %1$s</string> + <string name="error_could_not_load_login_page">Не удалось загрузить страницу входа.</string> + <string name="error_loading_account_details">Не удалось загрузить данные учётной записи</string> + <string name="title_public_trending_statuses">Популярные посты</string> + <string name="instance_rule_info">Входя в систему, вы соглашаетесь с правилами %1$s.</string> + <string name="instance_rule_title">%1$s правила</string> + <string name="pref_reading_order_oldest_first">Сначала старые</string> + <string name="pref_title_reading_order">Порядок чтения</string> + <string name="status_created_at_now">сейчас</string> + <string name="pref_ui_text_size">Размер текста пользовательского интерфейса</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="label_image">Изображение</string> + <string name="duration_90_days">90 дней</string> + <string name="no_lists">У вас нет никаких списков.</string> + <string name="delete_scheduled_post_warning">Удалить этот запланированный пост\?</string> + <string name="action_unfollow_hashtag_format">Отменить подписку на #%1$s\?</string> + <string name="failed_to_pin">Не удалось прикрепить</string> + <string name="post_media_image">Изображение</string> + <string name="status_filtered_show_anyway">Показать в любом случае</string> + <string name="status_filter_placeholder_label_format">Отфильтровано: %1$s</string> + <string name="mute_notifications_switch">Отключение звука уведомлений</string> + <string name="dialog_follow_hashtag_title">Подписаться на хэштег</string> + <string name="notification_update_format">%1$s отредактировал свое сообщение</string> + <string name="dialog_follow_hashtag_hint">#hashtag</string> + <string name="pref_summary_http_proxy_disabled">Отключено</string> + <string name="pref_summary_http_proxy_missing"><не задано></string> + <string name="pref_summary_http_proxy_invalid"><недействителен></string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="notification_listenable_worker_description">Уведомлять когда Tusky работает в фоновом режиме</string> + <string name="notification_notification_worker">Получаю уведомления…</string> + <string name="notification_listenable_worker_name">Фоновая деятельность</string> + <string name="dialog_push_notification_migration_other_accounts">Вы повторно вошли в свою текущую учетную запись, чтобы дать разрешение Tusky на рассылку push-уведомлений. Однако у вас все еще есть другие учетные записи, которые не были перенесены подобным образом. Переключитесь на них и заново войдите по очереди, чтобы включить поддержку UnifiedPush-уведомлений.</string> + <string name="total_accounts">Всего аккаунтов</string> + <string name="description_post_language">Язык записи</string> + <string name="pref_default_post_language">Язык постов по умолчанию</string> + <string name="notification_update_description">Уведомлять о редактировании постов в которых вы участвовали</string> + <string name="notification_update_name">Правки постов</string> + <string name="notification_report_name">Жалобы</string> + <string name="about_account_info_title">Ваш аккаунт</string> + <string name="about_account_info">\@%1$s@%2$s +\nВерсия: %3$s</string> + <string name="about_device_info_title">Ваше устройство</string> + <string name="set_focus_description">Нажмите или перетащите круг, чтобы выбрать пространство, которую всегда будет видно в миниатюрах.</string> + <string name="about_device_info">%1$s %2$s +\nВерсия Android: %3$s +\nВерсия SDK: %4$d</string> + <string name="pref_show_self_username_always">Всегда</string> + <string name="pref_title_show_self_username">Показывать имя пользователя на панели инструментов</string> + <string name="ui_error_reblog">Не удалось поделится этим постом: %1$s</string> + <string name="hint_filter_title">Мой фильтр</string> + <string name="label_filter_title">Название</string> + <string name="filter_action_warn">Предупреждение</string> + <string name="ui_success_rejected_follow_request">Запрос на подписку блокирован</string> + <string name="action_add">Добавить</string> + <string name="filter_keyword_display_format">%1$s (целое слово)</string> + <string name="label_filter_keywords">Ключевые слова или фразы для фильтрации</string> + <string name="action_details">Подробности</string> + <string name="action_dismiss">Отклонить</string> + <string name="hint_media_description_missing">У медиа должно быть описание.</string> + <string name="pref_show_self_username_disambiguate">Если вы вошли в несколько учетных записей</string> + <string name="saving_draft">Сохраняю черновик…</string> + <string name="tips_push_notification_migration">Заново войдите во все учетные записи, чтобы включить поддержку push-уведомлений.</string> + <string name="pref_title_account_filter_keywords">Профили</string> + <string name="failed_to_add_to_list">Не удалось добавить учетную запись в список</string> + <string name="duration_365_days">365 дней</string> + <string name="accessibility_talking_about_tag">%1$d людей говорят о хэштеге %2$s</string> + <string name="error_media_playback">Воспроизведение не удалось: %1$s</string> + <string name="dialog_delete_filter_positive_action">Удалить</string> + <string name="dialog_delete_filter_text">Удалить фильтр \'%1$s\'\?</string> + <string name="duration_60_days">60 дней</string> + <string name="pref_title_http_proxy_port_message">Порт должен находиться в диапазоне от %1$d до %2$d</string> + <string name="action_post_failed">Загрузка не удалась</string> + <string name="action_post_failed_detail">Ваше сообщение не удалось загрузить и оно было сохранено в черновиках. +\n +\nЛибо с сервером не удалось связаться, либо он отклонил сообщение.</string> + <string name="action_post_failed_detail_plural">Ваши сообщения не удалось загрузить и они были сохранены в черновиках. +\n +\nЛибо с сервером не удалось связаться, либо он отклонил сообщения.</string> + <string name="action_post_failed_show_drafts">Показать черновики</string> + <string name="action_post_failed_do_nothing">Отклонить</string> + <string name="duration_30_days">30 дней</string> + <string name="notification_unknown_name">Неизвестно</string> + <string name="post_media_alt">ALT</string> + <string name="duration_no_change">(Без изменений)</string> + <string name="duration_180_days">180 дней</string> + <string name="error_unmuting_hashtag_format">Не удалось вернуть звук #%1$s</string> + <string name="ui_error_bookmark">Не удалось добавить пост в закладки: %1$s</string> + <string name="ui_error_clear_notifications">Очистка уведомлений не удалась: %1$s</string> + <string name="ui_error_favourite">Не удалось отдать предпочтение сообщению: %1$s</string> + <string name="filter_action_hide">Скрыть</string> + <string name="label_filter_action">Отфильтровать действие</string> + <string name="label_filter_context">Фильтр контекстов</string> + <string name="filter_keyword_addition_title">Добавить ключевое слово</string> + <string name="filter_edit_keyword_title">Редактировать ключевое слово</string> + <string name="failed_to_remove_from_list">Не удалось удалить учетную запись из списка</string> + <string name="description_login">Работает в большинстве случаев. Данные не попадают в другие приложения.</string> + <string name="description_browser_login">Может поддерживать дополнительные методы аутентификации, но требует наличия браузера с его поддержкой.</string> + <string name="app_theme_system_black">Как в системе (черный)</string> + <string name="tusky_compose_post_quicksetting_label">Создать пост</string> + <string name="account_date_joined">Присоединился %1$s</string> + <string name="socket_timeout_exception">Связь с вашим сервером заняла слишком много времени</string> + <string name="about_copy">Копирование версии и информации об устройстве</string> + <string name="about_copied">Версия и информация об устройстве скопировано</string> + <string name="action_add_reaction">добавить реакцию</string> + <string name="select_list_manage">Управлять списками</string> + <string name="ui_error_reject_follow_request">Отклонение запроса на подписку не удалось: %1$s</string> + <string name="ui_error_accept_follow_request">Принятие запроса на подписку не удалось: %1$s</string> + <string name="ui_error_vote">Голосование в опросе не удалось: %1$s</string> + <string name="action_discard">Отбросить изменения</string> + <string name="action_continue_edit">Продолжить править</string> + <string name="notification_sign_up_name">Регистрации</string> + <string name="notification_sign_up_description">Уведомлять о новых пользователях</string> + <string name="action_share_account_link">Поделиться ссылкой на учетную запись</string> + <string name="action_share_account_username">Поделитесь именем пользователя учетной записи</string> + <string name="send_account_link_to">Поделится URL-адресом аккаунта в…</string> + <string name="send_account_username_to">Поделится именем пользователя в…</string> + <string name="account_username_copied">Имя пользователя скопировано</string> + <string name="post_edited">Отредактировано %1$s</string> + <string name="notification_header_report_format">%1$s пожаловался %2$s</string> + <string name="notification_report_format">Новая жалоба на %1$s</string> + <string name="notification_summary_report_format">%1$s - %2$d сообщений прикреплено</string> + <string name="confirmation_hashtag_unfollowed">отписался от #%1$s</string> + <string name="pref_title_notification_filter_sign_ups">кто-то зарегистрировался</string> + <string name="pref_title_notification_filter_updates">сообщение, на которое я отвечал, было отредактировано</string> + <string name="pref_title_notification_filter_reports">появилась новая жалоба</string> + <string name="description_post_edited">Отредактировано</string> + <string name="title_followed_hashtags">Отслеживаемые хэштэги</string> + <string name="error_status_source_load">Не удалось загрузить источник состояния с сервера.</string> + <string name="action_add_or_remove_from_list">Добавить или удалить из списка</string> + <string name="failed_to_unpin">Не удалось открепить</string> + <string name="muting_hashtag_success_format">Заглушить хэштег #%1$s в качестве предупреждения</string> + <string name="unmuting_hashtag_success_format">Включить обратно хэштег #%1$s</string> + <string name="action_view_filter">Посмотреть фильтр</string> + <string name="following_hashtag_success_format">Теперь хэштег #%1$s отслеживается</string> + <string name="unfollowing_hashtag_success_format">Больше не следите за хэштегом #%1$s</string> + <string name="report_category_violation">Нарушение правил</string> + <string name="action_set_focus">Установите фокус</string> + <string name="dialog_push_notification_migration">Чтобы использовать push-уведомления через UnifiedPush Tusky нужно разрешение на отслеживание уведомлений на вашем сервере Mastodon. Это требует повторного входа в систему для изменения ключей OAuth предоставленных Tusky. Использование опции повторного входа здесь или в настройках аккаунта сохранит все ваши локальные черновики и кэш.</string> + <string name="help_empty_home">Это ваша <b>основная хронология</b>. Здесь отображаются последние сообщения от аккаунтов на которые вы подписаны. +\n +\nЧтобы исследовать учетные записи, вы можете открыть их одной из других вкладок. Например, в местной ленте вашего экземпляра [iconics gmd_group]. Либо вы можете поискать их по имени [iconics gmd_search]; например, по имени Tusky вы найдете наш аккаунт Mastodon.</string> + <string name="help_empty_conversations">Здесь находятся ваши <b>личные сообщения</b>; иногда их называют беседами или прямыми сообщениями (ПС). +\n +\nЛичные сообщения создаются путем установки видимости [iconics gmd_public] сообщения на [iconics gmd_mail] <i>Прямые</i> и упоминания одного или нескольких пользователей в тексте. +\n +\nНапример, вы можете зайти в профиль аккаунта, нажать кнопку создания [iconics gmd_edit] и изменить видимость. </string> + <string name="help_empty_lists">Это ваш <b> обзор списков</b>. Вы можете сформировать ряд личных списков и добавить в них аккаунты. +\n +\nОбратите внимание, что в списки можно добавлять только те аккаунты на которые вы подписаны. +\n +\nЭти списки можно использовать в качестве вкладки в настройках аккаунта [iconics gmd_account_circle] [iconics gmd_navigate_next] Вкладки. </string> + <string name="compose_unsaved_changes">У вас есть несохраненные изменения.</string> + <string name="status_count_one_plus">1+</string> + <string name="compose_save_draft_loses_media">Сохранить черновик\? (Вложения будут загружены снова при восстановлении черновика).</string> + <string name="title_public_trending_hashtags">Популярные хэштеги</string> + <string name="notification_sign_up_format">%1$s зарегистрировался</string> + <string name="pref_title_show_stat_inline">Показать статистику поста в ленте</string> + <string name="filter_description_warn">Скрыть с предупреждением</string> + <string name="title_migration_relogin">Повторный вход в систему для получения push-уведомлений</string> + <string name="ui_error_unknown">неизвестная причина</string> + <string name="error_blocking_domain">Не удалось заглушить %1$s: %2$s</string> + <string name="error_unblocking_domain">Не удалось вернуть звук %1$s: %2$s</string> + <string name="notification_report_description">Уведомлять о жалобах модерации</string> + <string name="load_newest_notifications">Загрузка самых новых уведомлений</string> + <string name="compose_delete_draft">Удалить черновик\?</string> + <string name="error_missing_edits">Ваш сервер знает, что это сообщение было отредактировано, но у него нет копии правок, поэтому они не могут быть вам показаны. +\n +\nЭто <a href="https://github.com/mastodon/mastodon/issues/25398">Mastodon issue#25398</a>.</string> + <string name="list_reply_policy_none">Никому</string> + <string name="list_reply_policy_list">Члены списка</string> + <string name="list_reply_policy_followed">Любой пользователь на которого вы подписаны</string> + <string name="list_reply_policy_label">Показать ответы</string> + <string name="pref_title_show_self_boosts">Показать самоцитирования</string> + <string name="pref_title_show_self_boosts_description">Кто-то цитирует свой собственный пост</string> + <string name="pref_show_self_username_never">Никогда</string> + <string name="notification_prune_cache">Осматриваю кэш…</string> + <string name="list_exclusive_label">Скрыть с главной ленты</string> + <string name="duration_14_days">14 дней</string> + <string name="report_category_spam">Спам</string> + <string name="report_category_other">Другое</string> + <string name="action_refresh">Обновить</string> + <string name="total_usage">Всего использовано</string> + <string name="ui_success_accepted_follow_request">Запрос на подписку принят</string> + <string name="filter_description_hide">Скрыть полностью</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="dialog_save_profile_changes_message">Хотите сохранить изменения профиля\?</string> + <string name="action_edit_image">Редактировать изображение</string> + <string name="pref_title_per_timeline_preferences">Временные предпочтения</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-sa/strings.xml b/app/src/main/res/values-sa/strings.xml new file mode 100644 index 0000000..2421f0a --- /dev/null +++ b/app/src/main/res/values-sa/strings.xml @@ -0,0 +1,561 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="title_public_local">स्थानीयाः</string> + <string name="title_notifications">ज्ञापनसूचनाः</string> + <string name="title_home">गृहम्</string> + <string name="error_sender_account_gone">दौत्यप्रेषणं विफलं जातम् ।</string> + <string name="error_media_upload_sending">उपारोपणं विफलं जातम् ।</string> + <string name="error_media_upload_image_or_video">चलचित्राणि चित्राणि चोभे एव नैकस्मिन्नेव प्रकटने संस्थापिते भवितुमर्हतः ।</string> + <string name="error_media_download_permission">श्रव्यदृश्यसामग्र्यः रक्षयितुमनुमतिर्दातव्या ।</string> + <string name="error_compose_character_limit">भृशं दीर्घतमा स्थितिरियम् !</string> + <string name="error_media_upload_permission">श्रव्यदृश्यसामग्र्यः द्रष्टुमनुमतिर्दातव्या ।</string> + <string name="error_media_upload_opening">सा सञ्चिका नोद्घाट्यते ।</string> + <string name="error_media_upload_type">नैतादृशा सञ्चिका उपारोपणीया ।</string> + <string name="error_retrieving_oauth_token">सम्प्रवेशस्तोकं न लब्धम् ।</string> + <string name="error_authorization_denied">प्रमाणीकरणं निषिद्धम् ।</string> + <string name="error_authorization_unknown">अज्ञातः प्रमाणीकरणदोषो जातः ।</string> + <string name="error_no_web_browser_found">प्रयोजनार्थं जालसञ्चारकं न लब्धम् ।</string> + <string name="error_failed_app_registration">तेन विशिष्टस्थलेन प्रमाणीकरणं विफलं जातम् ।</string> + <string name="error_invalid_domain">अवैधानिकप्रदेशः प्रविष्टः</string> + <string name="error_empty">नैतद्रिक्तं भवितुमर्हति ।</string> + <string name="error_network">दोषो जातः । कृपया भवतोऽन्तर्जालीयसम्पर्कं परीक्ष्य पुनश्च यत्यताम् !</string> + <string name="error_generic">दोषो जातः ।</string> + <string name="footer_empty">न किमप्यत्र । नवीकरणार्थमाकृष्यतामधः !</string> + <string name="message_empty">न किमप्यत्र ।</string> + <string name="post_content_show_less">संनिपत्यताम्</string> + <string name="post_content_show_more">विस्तार्यताम्</string> + <string name="post_content_warning_show_less">स्वल्पं दृश्यताम्</string> + <string name="post_content_warning_show_more">अधिकं दृश्यताम्</string> + <string name="post_sensitive_media_directions">द्रष्टुमत्र नुद्यताम्</string> + <string name="post_media_hidden_title">प्रच्छन्नसामग्र्यः</string> + <string name="post_sensitive_media_title">संवेदनशीलो विषयः</string> + <string name="post_boosted_format">%1$s अप्रकाशयत्</string> + <string name="post_username_format">\@%1$s</string> + <string name="title_licenses">अनुज्ञापत्राणि</string> + <string name="title_scheduled_posts">कालबद्धदौत्यानि</string> + <string name="title_drafts">लेखविकर्षाः</string> + <string name="title_edit_profile">स्वीयव्यक्तिविवरणं सम्पाद्यताम्</string> + <string name="title_follow_requests">अनुसरणार्थमनुरोधाः</string> + <string name="title_domain_mutes">प्रच्छन्नप्रदेशाः</string> + <string name="title_blocks">निषिद्धोपभोक्तारः</string> + <string name="title_mutes">मूकोपभोक्तारः</string> + <string name="title_bookmarks">पुटचिह्नानि</string> + <string name="title_favourites">प्रियाः</string> + <string name="title_followers">अनुसर्तारः</string> + <string name="title_follows">अनुसरति</string> + <string name="title_posts_pinned">कीलिताः</string> + <string name="title_posts_with_replies">सप्रत्युत्तरम्</string> + <string name="title_posts">दौत्यानि</string> + <string name="title_view_thread">दौत्यमाला</string> + <string name="title_tab_preferences">पीठिकाः</string> + <string name="title_direct_messages">प्रत्यक्षसन्देशाः</string> + <string name="title_public_federated">सङ्घीयाः</string> + <string name="notification_follow_format">%1$s त्वामन्वसरत्</string> + <string name="notification_favourite_format">%1$s भवदीयदौत्याय रुचिमददात्</string> + <string name="notification_reblog_format">%1$s भवदीयं दौत्यं प्राकाशयत्</string> + <string name="action_send_public">प्रेष्यताम् !</string> + <string name="action_send">प्रेष्यताम्</string> + <string name="action_delete_and_redraft">विनश्य पुनश्च लिख्यताम्</string> + <string name="action_delete">नश्यताम्</string> + <string name="action_edit">सम्पाद्यताम्</string> + <string name="action_report">आवेद्यताम्</string> + <string name="action_show_reblogs">प्रकाशनानि दृश्यन्ताम्</string> + <string name="action_hide_reblogs">प्रकाशनानि छाद्यन्ताम्</string> + <string name="action_unblock">अवरोधो नश्यताम्</string> + <string name="action_block">अवरुध्यताम्</string> + <string name="action_unfollow">अनुसरणं अपाकुरुताम्</string> + <string name="action_follow">अनुस्रियताम्</string> + <string name="action_logout_confirm">नूनमेव बहिर्गन्तुमीहते %1$s इति व्यक्तित्वलेखात् \?</string> + <string name="action_logout">बहिर्गम्यताम्</string> + <string name="action_login">मास्टुडोनमाध्यमेन सम्प्रविश्यताम्</string> + <string name="action_compose">रच्यताम्</string> + <string name="action_more">अधिकम्</string> + <string name="action_unfavourite">प्रियता निष्क्रियताम्</string> + <string name="action_bookmark">पुटचिह्नं क्रियताम्</string> + <string name="action_favourite">प्रियम्</string> + <string name="action_unreblog">प्रकाशनं निष्क्रियताम्</string> + <string name="action_reblog">प्रकाश्यताम्</string> + <string name="action_reply">प्रत्युत्तरं दीयताम्</string> + <string name="action_quick_reply">त्वरितप्रत्युत्तरं दीयताम्</string> + <string name="report_comment_hint">अन्यटिप्पण्यः \?</string> + <string name="report_username_format">आवेद्यताम् @%1$s</string> + <string name="notification_follow_request_format">%1$s तवाऽनुसरणार्थंं न्यवेदयत्</string> + <string name="action_add_media">सामग्र्यः युज्यन्ताम्</string> + <string name="action_open_in_web">जालसञ्चारके उद्घाट्यताम्</string> + <string name="action_view_media">सामग्र्यः</string> + <string name="action_view_follow_requests">अनुसरणार्थमनुरोधाः</string> + <string name="action_view_domain_mutes">प्रच्छन्नप्रदेशाः</string> + <string name="action_view_blocks">निषिद्धभोक्तारः</string> + <string name="action_view_mutes">मूकभोक्तारः</string> + <string name="action_view_bookmarks">पुटचिह्नानि</string> + <string name="action_view_favourites">प्रियाः</string> + <string name="action_view_account_preferences">लेखाविन्यासाः</string> + <string name="action_view_preferences">विन्यासाः</string> + <string name="action_view_profile">व्यक्तिविवरणम्</string> + <string name="action_close">पिधीयताम्</string> + <string name="action_retry">पुनः यत्यताम्</string> + <string name="action_unmute_desc">%1$s सशब्दं क्रियताम्</string> + <string name="action_unmute">सशब्दम्</string> + <string name="action_mute">निःशब्दम्</string> + <string name="action_share">विभाज्यताम्</string> + <string name="action_photo_take">चित्रं गृह्यताम्</string> + <string name="action_add_poll">मतदानं युज्यताम्</string> + <string name="send_media_to">सामग्र्यस्मै विभाज्यताम् …</string> + <string name="send_post_content_to">दौत्यमस्मै विभाज्यताम् …</string> + <string name="send_post_link_to">दौत्यजालस्थलमस्मै विभाज्यताम् …</string> + <string name="downloading_media">सामग्री अवारोप्यमाणा</string> + <string name="download_media">सामग्री अवारोप्यताम्</string> + <string name="action_share_as">एवं विभाज्यताम् …</string> + <string name="action_open_as">%1$s एवमुद्घाट्यताम्</string> + <string name="action_copy_link">जालस्थलं प्रतिलिख्यताम्</string> + <string name="download_image">अवारोप्यमाणम् %1$s</string> + <string name="action_open_media_n">उद्घाट्यताम् #%1$d</string> + <string name="title_links_dialog">जालस्थलानि</string> + <string name="title_mentions_dialog">उल्लेखाः</string> + <string name="title_hashtags_dialog">निश्रेणिचिह्नशीर्षकाः</string> + <string name="action_open_faved_by">प्रियाणि दृश्यन्ताम्</string> + <string name="action_open_reblogged_by">प्रकाशनानि दृश्यन्ताम्</string> + <string name="action_open_reblogger">प्रकाशनलेखकः उद्घाट्यताम्</string> + <string name="action_hashtags">निश्रेणिचिह्नशीर्षकाः</string> + <string name="action_mentions">उल्लेखाः</string> + <string name="action_links">जालस्थलानि</string> + <string name="action_add_tab">पीठिका युज्यताम्</string> + <string name="action_reset_schedule">पुनरारम्भः</string> + <string name="action_schedule_post">कालबद्धदौत्यं क्रियताम्</string> + <string name="action_emoji_keyboard">भावचिह्नटङ्कणफलकम्</string> + <string name="action_content_warning">विषयप्रत्यादेशः</string> + <string name="action_toggle_visibility">दौत्यसुदर्शता</string> + <string name="action_access_scheduled_posts">कालबद्धदौत्यानि</string> + <string name="action_access_drafts">लेखविकर्षाः</string> + <string name="action_search">अन्विष्यताम्</string> + <string name="action_reject">अस्वीक्रियताम्</string> + <string name="action_accept">स्वीक्रियताम्</string> + <string name="action_undo">अपाक्रियताम्</string> + <string name="action_edit_own_profile">सम्पाद्यताम्</string> + <string name="action_edit_profile">व्यक्तिविवरणं सम्पाद्यताम्</string> + <string name="action_save">रक्ष्यताम्</string> + <string name="action_open_drawer">पेटिकोद्घट्यताम्</string> + <string name="action_hide_media">सामग्र्यः वार्यन्ताम्</string> + <string name="action_mention">उल्लिख्यताम्</string> + <string name="action_unmute_conversation">सशब्द आलापः क्रियताम्</string> + <string name="action_mute_conversation">तूष्णीमालापः क्रियताम्</string> + <string name="action_unmute_domain">%1$s सशब्दं क्रियताम्</string> + <string name="action_mute_domain">%1$s निःशब्दं क्रियताम्</string> + <string name="confirmation_unmuted">भोक्ता सशब्दः कृतः</string> + <string name="confirmation_unblocked">निरवरोधः कृतः</string> + <string name="confirmation_reported">प्रेषितम्!</string> + <string name="login_connection">सम्पर्कः क्रियते…</string> + <string name="link_whats_an_instance">किं नाम विशिष्टस्थलम्\?</string> + <string name="label_header">शीर्षः</string> + <string name="label_avatar">अवतारः</string> + <string name="label_quick_reply">प्रत्युत्तरम् …</string> + <string name="search_no_results">न परिणामाः</string> + <string name="hint_search">अन्विष्यताम्…</string> + <string name="hint_note">विवरणम्</string> + <string name="hint_display_name">नाम</string> + <string name="hint_content_warning">विषयप्रत्यादेशः</string> + <string name="hint_compose">किं वर्तमानमस्ति \?</string> + <string name="hint_domain">किं विशिष्टस्थलम् \?</string> + <string name="confirmation_domain_unmuted">%1$s अनावृतः</string> + <string name="pref_title_notification_filters">अहं ज्ञप्यतां यदा</string> + <string name="pref_title_notification_alert_light">ज्योत्या ज्ञप्यताम्</string> + <string name="pref_title_notification_alert_vibrate">कम्पनेन ज्ञप्यताम्</string> + <string name="pref_title_notification_alert_sound">ध्वनिना ज्ञप्यताम्</string> + <string name="pref_title_notification_alerts">सतर्कत-ज्ञापनसूचनाः</string> + <string name="pref_title_notifications_enabled">ज्ञापनसूचनाः</string> + <string name="pref_title_edit_notification_settings">ज्ञापनसूचनाः</string> + <string name="visibility_direct">प्रत्यक्षम् - केवलमुल्लिखितयोक्तृभ्यः प्रकट्यताम्</string> + <string name="visibility_private">केवलमनुसर्तृृणाम् :- कृते प्रकट्यताम्</string> + <string name="visibility_unlisted">अनिर्दिष्टः = सार्वजनिकसमयतालिकायां मा प्रकट्यताम्</string> + <string name="visibility_public">सार्वजनिकः= प्रकट्यतां सार्वजनिकसमयतालिकासु</string> + <string name="dialog_mute_hide_notifications">ज्ञापनसूचनाः छाद्यन्ताम्</string> + <string name="dialog_mute_warning">निःशब्दं क्रियताम् @%1$s\?</string> + <string name="dialog_block_warning">किल अवरुध्यताम् @%1$s\?</string> + <string name="mute_domain_warning_dialog_ok">प्रदेशः छाद्यताम्</string> + <string name="mute_domain_warning">निश्चियेन सर्वमेव निषिद्धं भवेदेतस्य जनस्य %1$s \? कोऽपि विषयो न द्रष्टुं शक्यते तत्प्रदेशात् कस्यामपि समयतालिकायामुत वा ते सूचनापेटिकायाम् । भवदनुसर्तारः तस्मात्प्रदेशान्निष्क्रियन्ते ।</string> + <string name="dialog_redraft_post_warning">विनश्य पुनः लिख्यताम् \?</string> + <string name="dialog_delete_post_warning">दौत्यमेतन्नश्यताम्\?</string> + <string name="dialog_unfollow_warning">व्यक्तित्वविवरणलेखायाः अनुसरणम् अपाकरणीयं किम् \?</string> + <string name="dialog_download_image">अवारोप्यताम्</string> + <string name="dialog_message_uploading_media">उपारोप्यमाणम्…</string> + <string name="dialog_title_finishing_media_upload">सामग्रीणामुपारोपणसिद्धिः वर्तमाना</string> + <string name="dialog_whats_an_instance">कस्याऽपि विशिष्टस्थलस्य सङ्केतसूत्रमत्र टङ्कयितुं शक्यते mastodon.social, icosahedron.website, social.tchncs.de, तथेैव<a href="https://instances.social">अधिकम्</a> +\n +\nयदि युष्माकं व्यक्तिगतलेखाऽत्र न वर्तते तर्हि तस्य विशिष्टस्थलस्य नाम टङ्कयित्वा तत्र निर्मातुं शक्नुथ । +\n +\nविशिष्टस्थलमित्युक्ते स्थलमेकं यत्र युष्माकं लेखाः आश्रिताः, किन्तु साफल्येनैवाऽन्यविशिष्टस्थलीयैः सह सम्पर्कयितुं शक्यते । +\n +\nअधिकमत्र प्राप्यते <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="hint_search_people_list">येषामुसरणं करोषि तेष्वन्विष्यताम्</string> + <string name="action_delete_list">सूचिर्नश्यताम्</string> + <string name="action_rename_list">पुनः सूचिनामकरणं क्रियताम्</string> + <string name="action_create_list">सूचिः निर्मीयताम्</string> + <string name="error_delete_list">सूचिर्नष्टुमशक्या</string> + <string name="error_rename_list">पुनः सूचिनामकरणं कर्तुमशक्यम्</string> + <string name="error_create_list">सूचिनिर्माणं कर्तुमशक्यम्</string> + <string name="dialog_message_cancel_follow_request">अनुसरणानुरोधो नश्यताम् \?</string> + <string name="title_lists">सूचयः</string> + <string name="action_lists">सूचयः</string> + <string name="add_account_description">नवमास्टोडोनलेखा युज्यताम्</string> + <string name="add_account_name">नवलेखा युज्यताम्</string> + <string name="filter_add_description">शोधनार्थं वाक्यांशः</string> + <string name="filter_dialog_whole_word_description">यदा शब्दो वा वाक्यांशश्चिह्नरहितो भवति, तर्हि विन्यासोऽयं स्थाप्यते केवलं यदा पूर्णत्वेन शब्दसमानता वर्तते</string> + <string name="filter_dialog_whole_word">सर्वः शब्दः</string> + <string name="filter_dialog_update_button">नवीक्रियताम्</string> + <string name="filter_dialog_remove_button">नश्यताम्</string> + <string name="filter_edit_title">शोधकं सम्पाद्यताम्</string> + <string name="filter_addition_title">शोधकं युज्यताम्</string> + <string name="pref_title_thread_filter_keywords">आलापाः</string> + <string name="pref_title_public_filter_keywords">सार्वजनिकतालिकाः</string> + <string name="load_more_placeholder_text">अधिकमारोप्यताम्</string> + <string name="replying_to">\@%1$s मित्रायोत्तरम्</string> + <string name="title_media">सामग्र्यः</string> + <string name="pref_title_alway_open_spoiler">सर्वदा विषयसतर्कतयाऽङ्कितं दौत्यं विस्तार्यताम्</string> + <string name="pref_title_alway_show_sensitive_media">सर्वदा संवेदनशीलविषयो दृश्यताम्</string> + <string name="follows_you">त्वामनुसरति</string> + <string name="abbreviated_seconds_ago">%1$ds क्ष</string> + <string name="abbreviated_minutes_ago">%1$dm नि</string> + <string name="abbreviated_hours_ago">%1$dh घ</string> + <string name="abbreviated_years_ago">%1$dy वर्ष</string> + <string name="abbreviated_days_ago">%1$dd दि</string> + <string name="abbreviated_in_seconds">%1$ds क्ष</string> + <string name="abbreviated_in_minutes">%1$dm नि</string> + <string name="abbreviated_in_hours">%1$dh घ</string> + <string name="abbreviated_in_days">%1$dd दि</string> + <string name="abbreviated_in_years">%1$dy वर्ष</string> + <string name="state_follow_requested">अनुसरणं निवेदितम्</string> + <string name="post_media_video">चलचित्राणि</string> + <string name="post_media_images">चित्राणि</string> + <string name="post_share_link">दौत्याय जालस्थानं विभाज्यताम्</string> + <string name="post_share_content">दौत्यविषयो विभाज्यताम्</string> + <string name="about_tusky_account">टस्कीवर्यस्य व्यक्तिगतविवरणम्</string> + <string name="about_bug_feature_request_site">अशुद्धीनामावेदनं वैशिष्ट्यनिवेदनञ्च +\n https://github.com/tuskyapp/Tusky/issues</string> + <string name="about_project_site">प्रकल्पस्य जालसूत्रम् : +\n https://tusky.app</string> + <string name="about_tusky_license">टस्कीत्यनावृतस्रोतो निःशुल्कतन्त्रांशः। GNU General Public License Version 3 इत्यनेनाऽनुज्ञापितः। अत्राऽनुज्ञापत्रं द्रष्टुं शक्यते:-https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="about_powered_by_tusky">टस्कीत्यनेनाऽऽश्रितः</string> + <string name="about_tusky_version">टस्की %1$s</string> + <string name="about_title_activity">विज्ञप्तिः</string> + <string name="description_account_locked">कपाटितव्यक्तिविवरणलेखः</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d नवपरस्परक्रिया</item> + <item quantity="other">%1$d नवपरस्परक्रिये</item> + </plurals> + <string name="notification_summary_small">%1$s च %2$s च</string> + <string name="notification_summary_medium">%1$s, %2$s, तथैव %3$s</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s तथा च %4$d अन्येऽपि</string> + <string name="notification_mention_format">%1$s मित्रेण भवन्नामोल्लिखितम्</string> + <string name="notification_poll_description">मतदाने समाप्ते सति सूचनाः</string> + <string name="notification_poll_name">मतदानानि</string> + <string name="notification_favourite_description">प्रीतिः इत्यङ्किते सति ज्ञापनसूचनाः</string> + <string name="notification_favourite_name">प्रियाः</string> + <string name="notification_boost_description">दौत्यप्रकाशने सति ज्ञापनसूचनाः</string> + <string name="notification_boost_name">प्रकाशनानि</string> + <string name="notification_follow_request_description">अनुसरणानुरोधान्नधिकृत्य ज्ञापनसूचनाः</string> + <string name="notification_follow_request_name">अनुसरणार्थमनुरोधाः</string> + <string name="notification_mention_descriptions">नवोल्लेखान्नधिकृत्य ज्ञापनसूचनाः</string> + <string name="notification_follow_description">नवानुसर्तृृन्नधिकृत्य ज्ञापनसूचनाः</string> + <string name="notification_follow_name">नवानुसर्तारः</string> + <string name="notification_mention_name">नवोल्लेखाः</string> + <string name="post_text_size_largest">स्थूलतमः</string> + <string name="post_text_size_large">स्थूलः</string> + <string name="post_text_size_medium">मध्यमः</string> + <string name="post_text_size_small">सूक्ष्मः</string> + <string name="post_text_size_smallest">सूक्ष्मतमः</string> + <string name="pref_post_text_size">दौत्यस्य / स्थितेरक्षराकारः</string> + <string name="post_privacy_followers_only">केवलमनुसर्तृभ्यः</string> + <string name="post_privacy_unlisted">अनिर्दिष्टम्</string> + <string name="post_privacy_public">सार्वजनिकम्</string> + <string name="pref_main_nav_position_option_bottom">नितलम्</string> + <string name="pref_main_nav_position_option_top">शिखरम्</string> + <string name="pref_main_nav_position">मुख्यमार्गणस्थितिः</string> + <string name="pref_failed_to_sync">विन्यासं समसामयिकं कर्तुं विफलता</string> + <string name="pref_publishing">प्रकाशनम् (जालवितरकेण सह सामयिकम्)</string> + <string name="pref_default_media_sensitivity">श्रव्यदृश्यसामग्रीः सदा संवेदनशीलाः इत्यङ्क्यताम्</string> + <string name="pref_default_post_privacy">पूर्वनिविष्टप्रकटनगुह्यता</string> + <string name="pref_title_http_proxy_port">HTTPS प्रतिनिधिद्वारिका</string> + <string name="pref_title_http_proxy_server">HTTPS प्रतिनिधिजालवितारकम्</string> + <string name="pref_title_http_proxy_enable">HTTP प्रतिनिधिसंयुतनं क्रियताम्</string> + <string name="pref_title_http_proxy_settings">HTTP प्रतिनिधिः</string> + <string name="pref_title_proxy_settings">प्रतिनिधिः</string> + <string name="pref_title_show_media_preview">सामग्रीणां पूर्वोद्घाटनमवारोप्यताम्</string> + <string name="pref_title_show_replies">प्रत्युत्तराणि दृश्यन्ताम्</string> + <string name="pref_title_show_boosts">प्रकाशनानि दृश्यन्ताम्</string> + <string name="pref_title_post_tabs">पीठिकाः</string> + <string name="pref_title_post_filter">समयतालिका-शोधनम्</string> + <string name="pref_title_gradient_for_media">छादितसामग्रीभ्यो बहुवर्णयुतचित्रं दर्शयतु</string> + <string name="pref_title_animate_gif_avatars">सञ्जीवितावतारः क्रियताम्</string> + <string name="pref_title_bot_overlay">स्वचालितयन्त्रेभ्यः सूचको दृश्यताम्</string> + <string name="pref_title_language">भाषा</string> + <string name="pref_title_custom_tabs">क्रोमस्वीयानुकूलपीठिकाः प्रयुज्यन्ताम्</string> + <string name="pref_title_browser_settings">जालसञ्चारकम्</string> + <string name="app_theme_system">प्रणाल्याः परिकल्पना प्रयुज्यताम्</string> + <string name="app_theme_auto">सूर्यास्तसमये स्वचालितम्</string> + <string name="app_theme_black">कृष्णः</string> + <string name="app_theme_light">ज्योतिपूर्णः</string> + <string name="app_them_dark">अन्धकारः</string> + <string name="pref_title_timeline_filters">शोधकम्</string> + <string name="pref_title_timelines">समयतालिकाः</string> + <string name="pref_title_app_theme">अनुप्रयोगप्रबन्धाः</string> + <string name="pref_title_appearance_settings">स्वरूपम्</string> + <string name="pref_title_notification_filter_poll">मतदानं समाप्तम्</string> + <string name="pref_title_notification_filter_favourites">मम प्रकटनानि प्रियाणि</string> + <string name="pref_title_notification_filter_reblogs">मम प्रकटनानि प्रकाशितानि</string> + <string name="pref_title_notification_filter_follow_requests">अनुसरणार्थं निवेदितम्</string> + <string name="pref_title_notification_filter_follows">अनुसृतम्</string> + <string name="pref_title_notification_filter_mentions">उल्लिखिताः</string> + <string name="download_fonts">प्राक्तु भावचिह्नसमूहोऽयमवारोप्यः</string> + <string name="system_default">प्रणाल्यां पूर्वनिविष्टम्</string> + <string name="emoji_style">भावचिह्नशैली</string> + <string name="error_no_custom_emojis">भवदीयं विशिष्टस्थलं %1$s स्वीयानुकूलभावचिह्नरहितं वर्तते</string> + <string name="action_compose_shortcut">लिख्यताम्</string> + <string name="send_post_notification_saved_content">दौत्यप्रतिलिपिस्तत्र विकर्षेसु रक्षिता</string> + <string name="send_post_notification_cancel_title">प्रेषणं निराकृतम्</string> + <string name="send_post_notification_channel_name">प्रेष्यमाणानि</string> + <string name="send_post_notification_error_title">दौत्यप्रेषणे दोषः</string> + <string name="send_post_notification_title">दौत्यं प्रेष्यमाणम्…</string> + <string name="compose_save_draft">रक्षणीयम् \?</string> + <string name="lock_account_label_description">स्वयमेवाऽनुसर्तॄणां कृतेऽनुमतिर्दातव्या</string> + <string name="lock_account_label">लेखा अवरुध्यताम्</string> + <string name="action_remove">नश्यताम्</string> + <string name="action_set_caption">शीर्षकवाक्यं लिख्यताम्</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">दृष्ट्यां येषां समस्याऽस्ति तेषांं कृते विवरणम् +\n(%1$d परिमिता न्यूनाक्षरसङ्ख्या)</item> + </plurals> + <string name="compose_active_account_description">%1$s लेखया प्रकटनं क्रियते</string> + <string name="action_remove_from_list">सूच्याः लेखा नश्यताम्</string> + <string name="action_add_to_list">सूच्यां लेखा स्थाप्यताम्</string> + <string name="profile_badge_bot_text">यन्त्रम्</string> + <string name="download_failed">अवारोपणे दोषः</string> + <string name="caption_notoemoji">गुगलस्य वर्तमानभावचिह्नसमूहः</string> + <string name="caption_twemoji">मास्टोडोनस्य पूर्वनिविष्टभावचिह्नसमूहः</string> + <string name="caption_blobmoji">ब्लाबभावचिह्नानि एन्ड्रोइड४.४तः ७.१पर्यन्तं प्रसिद्धानि</string> + <string name="caption_systememoji">तव यन्त्रस्य पूर्वस्थापितभावचिह्नसमूहः</string> + <string name="restart">पुनः प्रारभ्यताम्</string> + <string name="later">पश्चात्</string> + <string name="restart_emoji">पुनश्च टस्कीप्रारम्भोऽपेक्षितो वर्तते परिवर्तनानुसरेण चलितुम्</string> + <string name="restart_required">अनुप्रयोगप्रारम्भः आवश्यकः</string> + <string name="action_open_post">दौत्यमुद्घाट्यताम्</string> + <string name="expand_collapse_all_posts">विस्तार्यन्तां नश्यन्तां वा स्थतयः</string> + <string name="performing_lookup_title">अन्वेषणं भवद्वर्तते…</string> + <string name="pin_action">कीलयतु</string> + <string name="unpin_action">कीलनं नश्यताम्</string> + <string name="label_remote_account">अधो लिखितवार्तोपभोक्तुः व्यक्तित्वविवरणमांशिकरूपेण प्रतिबिम्बयेत् । व्यक्तित्वविवरणमुद्घाट्यतां कश्मिंश्चिदपि जालसञ्चारके ।</string> + <string name="pref_title_absolute_time">मूलसमयो युज्यताम्</string> + <string name="profile_metadata_content_label">विषयः</string> + <string name="profile_metadata_label_label">लक्षः</string> + <string name="profile_metadata_add">दत्तं युज्यताम्</string> + <string name="profile_metadata_label">व्यक्तिगतविवरणदत्तांशः</string> + <string name="license_cc_by_sa_4">CC-BY-SA ४.०</string> + <string name="license_cc_by_4">CC-BY ४.०</string> + <string name="license_apache_2">Apache Licence (अनुलिख्यताम्) इत्यनुज्ञप्तिपत्रेणाऽनुज्ञापितः</string> + <string name="license_description">टस्कीत्यस्मिन्निम्नलिखितेभ्योऽनावृतस्रोतःप्रकल्पेभ्यो विध्यादेशाः सन्ति:</string> + <string name="unreblog_private">प्रकाशनंं नश्यताम्</string> + <string name="reblog_private">मूलदर्शकेभ्यः प्रकाश्यताम्</string> + <string name="account_moved_description">%1$s मित्रमत्र प्रस्थितम्:</string> + <string name="no_drafts">न लेखविकर्षास्ते सन्ति ।</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="pref_title_hide_top_toolbar">उच्चैःस्थितायाः साधनशालकायाः शीर्षकं छाद्यताम्</string> + <string name="pref_title_confirm_reblogs">प्रकाशनात् प्राक् पुष्टिसंवादमञ्जूषा दर्शनीया</string> + <string name="pref_title_show_cards_in_timelines">जालस्थानप्रदर्शनं समयतालिकायां दर्शयतु</string> + <string name="warning_scheduling_interval">मास्टोडोने पञ्चनिमेषपरिमितो न्यूनतमः कालबद्धसमयः ।</string> + <string name="no_scheduled_posts">न ते कालबद्धदौत्यानि सन्ति ।</string> + <string name="edit_poll">सम्पाद्यताम्</string> + <string name="poll_new_choice_hint">मतम् %1$d</string> + <string name="poll_allow_multiple_choices">बहूनि मतानि</string> + <string name="add_poll_choice">अपरं मतं युज्यताम्</string> + <string name="duration_7_days">७ दिनानि</string> + <string name="duration_3_days">३ दिनानि</string> + <string name="duration_1_day">१ दिनम्</string> + <string name="duration_6_hours">६ घण्टाः</string> + <string name="duration_1_hour">१ घण्टा</string> + <string name="duration_30_min">३० निमेषाः</string> + <string name="duration_5_min">५ निमेषाः</string> + <string name="create_poll_title">मतपेटिका</string> + <string name="pref_title_enable_swipe_for_tabs">सारणहावभावस्य संयुतनं पीठिकापरिवर्तनार्थं कार्यम्</string> + <string name="failed_search">अन्वेषणे विफलता जाता</string> + <string name="title_accounts">व्यक्तित्वविवरणलेखाः</string> + <string name="report_description_remote_instance">अन्यजालवितारकादियं व्यक्तित्वविवरणलेखा । आवेदनस्य रक्षितप्रतिलिपिरपि प्रेष्यतां वा \?</string> + <string name="report_description_1">आवेदनमिदं जालवितारकाय प्रेष्यते । स्वीयविवरणमस्मिन् विषयेऽधो लेखितुं शक्नोषि-:</string> + <string name="failed_fetch_posts">दौत्यानि गृहीतुं विफलता</string> + <string name="failed_report">आवेदनप्रेषणे विफलता</string> + <string name="report_remote_instance">अस्मै पुरस्क्रियताम् %1$s</string> + <string name="hint_additional_info">अन्याः टिप्पण्यः</string> + <string name="report_sent_success">साफल्येनाऽऽवेदनं दत्तमस्य @%1$s</string> + <string name="button_done">कृतम्</string> + <string name="button_back">पूर्वम्</string> + <string name="button_continue">निरन्तरम्</string> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d क्षणम्</item> + <item quantity="other">%1$d क्षणे</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d निमेषः शेषम्</item> + <item quantity="other">%1$d निमेषौ शेषम्</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d घण्टा शेषम्</item> + <item quantity="other">%1$d घण्टे शेषम्</item> + </plurals> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d दिनम् शेषम्</item> + <item quantity="other">%1$d दिने शेषम्</item> + </plurals> + <string name="poll_ended_created">त्वया रचितमेकं मतदानं समाप्तम्</string> + <string name="poll_ended_voted">मतदानमेकं समाप्तं यस्मिन् त्वयाऽपि स्वीयमतं दत्तम्</string> + <string name="poll_vote">मतम्</string> + <string name="poll_info_closed">पिहितम्</string> + <string name="poll_info_time_absolute">समापनं यावत् %1$s</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s जनः</item> + <item quantity="other">%1$s जनौ</item> + </plurals> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s मतम्</item> + <item quantity="other">%1$s मते</item> + </plurals> + <string name="poll_info_format"> <!-- 15 मतानि • 1 शेषघटिकाः --> %1$s • %2$s</string> + <string name="compose_preview_image_description">चित्राणां कृते प्रवृत्तिः %1$s</string> + <string name="notification_clear_text">किं नूनमेव सर्वाः सूचनाः स्थायित्वेन मार्जनीयाः \?</string> + <string name="compose_shortcut_short_label">लिख्यताम्</string> + <string name="compose_shortcut_long_label">दौत्यं लिख्यताम्</string> + <string name="filter_apply">स्थाप्यताम्</string> + <string name="notifications_apply_filter">शुध्यताम्</string> + <string name="notifications_clear">मार्ज्यन्ताम्</string> + <string name="list">सूचिः</string> + <string name="select_list_title">सूचिरवचीयताम्</string> + <string name="hashtags">निश्रेणिचिह्नशीर्षकाः</string> + <string name="edit_hashtag_hint"># चिह्नं विना प्रचलितम्</string> + <string name="add_hashtag_title">प्रचलितं युज्यताम्</string> + <string name="hint_list_name">सूचिनाम</string> + <string name="description_poll">मतदाने मतानि- %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="description_visibility_direct">प्रत्यक्षम्</string> + <string name="description_visibility_private">अनुसर्तारः</string> + <string name="description_visibility_unlisted">अनिर्दिष्टम्</string> + <string name="description_visibility_public">सार्वजनिकम्</string> + <string name="description_post_bookmarked">पुटचिह्नं कृतम्</string> + <string name="description_post_favourited">प्रीतिर्दत्ता</string> + <string name="description_post_reblogged">पुनर्लिखितम्</string> + <string name="description_post_media_no_description_placeholder">विवरणं नास्ति</string> + <string name="description_post_cw">विषयपूर्वसतर्कता: %1$s</string> + <string name="description_post_media">सामग्र्यः %1$s</string> + <string name="conversation_more_recipients">%1$s, %2$s तथा %3$d अन्येऽपि</string> + <string name="conversation_2_recipients">%1$s तथैव %2$s</string> + <string name="title_favourited_by">निम्नमित्रस्य प्रीतिः</string> + <string name="title_reblogged_by">निम्नमित्रेण प्रकाशितम्</string> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> प्रकाशनम्</item> + <item quantity="other"><b>%1$s</b> प्रकाशने</item> + </plurals> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> प्रियम्</item> + <item quantity="other"><b>%1$s</b> प्रिये</item> + </plurals> + <string name="total_usage">समस्त-उपयोगः</string> + <string name="total_accounts">सकललेखाः</string> + <string name="a11y_label_loading_thread">दौत्यमाला दृश्यते</string> + <string name="instance_rule_title">%1$s नियमाः</string> + <string name="pref_title_reading_order">पाठनक्रमः</string> + <string name="pref_reading_order_oldest_first">पुरातनं प्रथमम्</string> + <string name="pref_reading_order_newest_first">नूतनं प्रथमम्</string> + <string name="duration_indefinite">अपरिमितम्</string> + <string name="action_set_focus">केन्द्रबिन्दुं स्थाप्यताम्</string> + <string name="mute_notifications_switch">सूचनाः निशब्दाः करोतु</string> + <string name="failed_to_pin">कीलनं विफलं जातम्</string> + <string name="failed_to_unpin">कीलनस्य अपाकरणं विफलं जातम्</string> + <string name="pref_summary_http_proxy_disabled">अशक्तं कृतम्</string> + <string name="pref_summary_http_proxy_invalid"><अप्रमाणम्></string> + <string name="action_edit_image">चित्रं सम्पाद्यताम्</string> + <string name="pref_summary_http_proxy_missing"><अनियुक्तम्></string> + <string name="duration_180_days">१८० दिनानि</string> + <string name="duration_365_days">३६५ दिनानि</string> + <string name="duration_no_change">(परिवर्तनं नास्ति)</string> + <string name="review_notifications">सूचनाः सम्दृश्यन्ताम्</string> + <string name="pref_show_self_username_never">न कदापि</string> + <string name="pref_default_post_language">पूर्वनिविष्टा प्रकाशका भाषा</string> + <string name="notification_report_name">आवेदनानि</string> + <string name="notification_update_name">सम्पादनानि निवेद्यन्ताम्</string> + <string name="pref_title_show_self_username">साधनशलाकासु उपभोक्तृनाम दृश्यताम्</string> + <string name="title_edits">सम्पादनानि</string> + <string name="notification_sign_up_description">नूतनोपभोक्तॄन् प्रति सूचनाः</string> + <string name="post_media_audio">ध्वनिः</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="duration_30_days">३० दिनानि</string> + <string name="report_category_other">अन्यम्</string> + <string name="title_login">सम्प्रवेशः</string> + <string name="dialog_delete_conversation_warning">इदं सम्भाषणं निष्कास्यताम् \?</string> + <string name="pref_title_notification_filter_sign_ups">कश्चन पञ्जीकरणम् अकरोत्</string> + <string name="pref_show_self_username_always">सर्वदा</string> + <string name="notification_subscription_name">नूतन-प्रकटनानि</string> + <string name="notification_sign_up_name">पञ्जीकरणानि</string> + <string name="no_announcements">उद्घोषणाः न सन्ति।</string> + <string name="status_created_at_now">अधुना</string> + <string name="duration_60_days">६० दिनानि</string> + <string name="action_unsubscribe_account">ग्राहकता-समापनम्</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="action_post_failed">उपारोपनं विफलं जातम्</string> + <string name="action_post_failed_do_nothing">उत्सृज्यताम्</string> + <string name="action_post_failed_show_drafts">लेखविकर्षान् दर्शयतु</string> + <string name="description_post_edited">सम्पादनं कृतम्</string> + <string name="status_created_info">%1$s निर्मितम्</string> + <string name="status_edit_info">%1$s सम्पादितम्</string> + <string name="action_subscribe_account">ग्राहकता</string> + <string name="draft_deleted">प्रारूपं निष्कासितम्</string> + <string name="report_category_violation">नियम-उल्लङ्घनम्</string> + <string name="error_muting_hashtag_format">#%1$s-निश्रेणिचिह्नशीर्षकस्य निशब्दीकरणे दोषः</string> + <string name="error_unmuting_hashtag_format">#%1$s-निश्रेणिचिह्नशीर्षकस्य सशब्दीकरणे दोषः</string> + <string name="action_browser_login">जालसञ्चारकेन सम्प्रविश्यताम्</string> + <string name="action_add_reaction">प्रतिक्रियां योजयतु</string> + <string name="notification_header_report_format">%1$s-व्यक्तिः %2$s-व्यक्तिं प्रति आवेदनमकरोत्</string> + <string name="action_discard">परिवर्तनानि निष्कासयतु</string> + <string name="action_continue_edit">सम्पादनम् अनुवर्तताम्</string> + <string name="post_edited">%1$s सम्पादितम्</string> + <string name="notification_subscription_format">%1$s सद्यः प्रसारितम्</string> + <string name="notification_sign_up_format">%1$s पञ्जीकरणम् अकरोत्</string> + <string name="action_delete_conversation">सम्भाषणं निष्कास्यताम्</string> + <string name="duration_90_days">९० दिनानि</string> + <string name="action_unbookmark">पुटचिह्नं निस्कास्यताम्</string> + <string name="notification_update_format">%1$s स्वस्य दौत्यं समपादयत्</string> + <string name="tusky_compose_post_quicksetting_label">दौत्यं संस्कुरुताम्</string> + <string name="saving_draft">लेखविकर्षं रक्षामि…</string> + <string name="account_date_joined">%1$s सदस्यः अभवत्</string> + <string name="account_username_copied">उपभोक्तृनाम्नः प्रतिकृतिः कृतः</string> + <string name="label_duration">समयः</string> + <string name="report_category_spam">अनिष्टसन्देशाः</string> + <string name="duration_14_days">१४ दिनानि</string> + <string name="description_post_language">दौत्यस्य भाषा</string> + <string name="error_following_hashtag_format">#%1$s-अनुसरणे दोषो जातः</string> + <string name="error_unfollowing_hashtag_format">#%1$s-अनुसरण-अपाकरणे दोषो जातः</string> + <string name="action_details">विवरणानि</string> + <string name="action_dismiss">उत्सृज्यताम्</string> + <string name="post_media_attachments">सम्योजितानि</string> + <string name="status_count_one_plus">१+</string> + <string name="account_note_saved">रक्षितम् !</string> + <string name="title_announcements">उद्घोषणाः</string> + <string name="error_image_edit_failed">चित्रं सम्पादयितुं न शक्यते।</string> + <string name="error_loading_account_details">व्यक्तित्वलेखस्य विवरणानि दर्शनं विफलं जातम्</string> + <string name="error_could_not_load_login_page">सम्प्रवेशपुटं दर्शयितुं न शक्यते।</string> + <string name="title_migration_relogin">पुनःसम्प्रवेशाय विज्ञापन-ज्ञापनसूचनाः</string> + <string name="dialog_follow_hashtag_hint">#निश्रेणिचिह्नशीर्षकः</string> + <string name="dialog_follow_hashtag_title">व्यक्तित्वविवरणलेखा अनुसरतु</string> + <string name="limit_notifications">कालानुक्रमपङ्क्त्याः सूचनाः परिमिताः कुरुताम्</string> + <string name="notification_report_description">परिमितावेदनानि प्रति ज्ञापनसूचनाः</string> + <string name="compose_unsaved_changes">भवतः अरक्षितानि परिवर्तनानि सन्ति।</string> + <string name="pref_title_notification_filter_reports">नूतनम् आवेदनमस्ति</string> + <string name="pref_title_wellbeing_mode">सुस्थितिः</string> + <string name="delete_scheduled_post_warning">इदं कालबद्धदौत्यं विनश्येत् किम् \?</string> + <string name="drafts_failed_loading_reply">प्रत्युत्तरसमाचारस्य आरोपनं विफलं जातम्</string> + <string name="post_media_alt">आल्ट्</string> + <string name="pref_title_animate_custom_emojis">स्वीयानुकूलानि भावचिह्नानि सञ्जीव्यताम्</string> + <string name="notification_report_format">%1$s-विषये नूतनम् आवेदनम्</string> + <string name="action_share_account_username">लेखायाः उपभोक्तृनाम्नः संविभागं कुरुताम्</string> + <string name="action_share_account_link">लेखायै शृङ्खलायाः संविभागं कुरुताम्</string> + <string name="action_unfollow_hashtag_format">#%1$s-चिह्नस्य अनुसरणम् अपाकरणीयं किम् \?</string> + <string name="title_followed_hashtags">अनुसृताः निश्रेणिचिह्नशीर्षकाः</string> + <string name="title_public_trending_hashtags">जनप्रियाः निश्रेणिचिह्नशीर्षकाः</string> + <string name="send_account_username_to">लेखायाः उपभोक्तृनाम्नः संविभागं कुरुताम् अस्मै…</string> + <string name="confirmation_hashtag_unfollowed">#%1$s अनुसरणम् अपाकृतम्</string> + <string name="send_account_link_to">लेखायाः निरपेक्ष-सार्वत्रिक-वस्तुसङ्केतस्य संविभागं कुरुताम् अस्मै…</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml new file mode 100644 index 0000000..c4721df --- /dev/null +++ b/app/src/main/res/values-si/strings.xml @@ -0,0 +1,267 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="conversation_2_recipients">%1$s සහ %2$s</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="notifications_apply_filter">පෙරහන</string> + <string name="filter_apply">යොදන්න</string> + <string name="button_back">ආපසු</string> + <string name="title_accounts">ගිණුම්</string> + <string name="duration_5_min">විනාඩි 5</string> + <string name="duration_30_min">විනාඩි 30</string> + <string name="duration_6_hours">පැය 6</string> + <string name="duration_1_hour">පැය 1</string> + <string name="duration_3_days">දවස් 3</string> + <string name="duration_1_day">දවස් 1</string> + <string name="duration_7_days">දවස් 7</string> + <string name="edit_poll">සංස්කරණය</string> + <string name="title_posts_with_replies">පිළිතුරු සමඟ</string> + <string name="title_blocks">අවහිර කළ පරිශීලකයින්</string> + <string name="title_drafts">කටුපිටපත්</string> + <string name="footer_empty">කිසිවක් නැත. නැවුම් කිරීමට පහළට අදින්න!</string> + <string name="action_quick_reply">ඉක්මන් පිළිතුර</string> + <string name="action_unbookmark">පොත්යොමුව ඉවත් කරන්න</string> + <string name="action_send">ටූට්</string> + <string name="action_view_blocks">අවහිර කළ පරිශීලකයින්</string> + <string name="action_open_in_web">අතිරික්සුවෙන් විවෘත කරන්න</string> + <string name="action_report">වාර්තා කරන්න</string> + <string name="search_no_results">ප්රතිඵල නැත</string> + <string name="login_connection">සම්බන්ධ වෙමින්…</string> + <string name="pref_title_notifications_enabled">දැනුම්දීම්</string> + <string name="post_privacy_public">ප්රසිද්ධ</string> + <string name="post_media_attachments">ඇමුණුම්</string> + <string name="notification_mention_name">නව සැඳහුම්</string> + <string name="action_compose_shortcut">රචනා කරන්න</string> + <string name="post_content_warning_show_more">තව පෙන්වන්න</string> + <string name="send_post_notification_saved_content">ටූට් හි පිටපතක් ඔබගේ කටුපිටපත් තුළට සුරකින ලදි</string> + <string name="action_hide_media">මාධ්ය සඟවන්න</string> + <string name="title_edit_profile">පැතිකඩ සංස්කරණය</string> + <string name="confirmation_reported">යැවිණි!</string> + <string name="action_reset_schedule">යළි සකසන්න</string> + <string name="post_content_warning_show_less">අඩුවෙන් පෙන්වන්න</string> + <string name="abbreviated_in_seconds">තත්. %1$d කින්</string> + <string name="action_view_mutes">නිහඬ කළ පරිශීලකයින්</string> + <string name="action_share">බෙදාගන්න</string> + <string name="account_moved_description">%1$s ගෙන ගොස් ඇත:</string> + <string name="post_share_link">ටූට් වෙත සබැඳියක් බෙදාගන්න</string> + <string name="title_licenses">බලපත්ර</string> + <string name="action_edit_profile">පැතිකඩ සංස්කරණය</string> + <string name="hint_display_name">දර්ශන නාමය</string> + <string name="post_text_size_medium">මධ්යම</string> + <string name="post_media_audio">ශ්රව්ය</string> + <string name="abbreviated_days_ago">දව. %1$d</string> + <string name="action_logout">නික්මෙන්න</string> + <string name="filter_edit_title">පෙරහන සංස්කරණය</string> + <string name="notification_summary_medium">%1$s, %2$s, සහ %3$s</string> + <string name="caption_twemoji">මාස්ටඩන් හි සම්මත ඉමෝජි කට්ටලය</string> + <string name="poll_info_format"> <!-- 15 votes • 1 hour left --> %1$s • %2$s</string> + <string name="action_retry">යළි උත්සාහය</string> + <string name="lock_account_label">ගිණුම අගුළුලන්න</string> + <string name="post_media_hidden_title">මාධ්ය සැඟවී ඇත</string> + <string name="title_public_federated">ඒකාබද්ධ</string> + <string name="dialog_message_uploading_media">උඩුගත වෙමින්…</string> + <string name="add_account_name">ගිණුම එකතු කරන්න</string> + <string name="label_header">ශීර්ෂය</string> + <plurals name="poll_timespan_hours"> + <item quantity="one">පැය %1$d ක් ඉතිරිය</item> + <item quantity="other">පැය %1$d ක් ඉතිරිය</item> + </plurals> + <string name="action_unfavourite">ප්රියතමය ඉවත් කරන්න</string> + <string name="drafts_failed_loading_reply">පිළිතුරෙහි තොරතුරු පූරණය වීමට අසමත් විය</string> + <string name="report_username_format">\@%1$s වාර්තා කරන්න</string> + <string name="report_comment_hint">අතිරේක අදහස්\?</string> + <string name="pref_title_notification_filter_mentions">සඳහන් කළ</string> + <string name="send_post_link_to">වෙත ටූට් ඒ.ස.නි. බෙදාගන්න…</string> + <string name="app_theme_light">දීප්ත</string> + <string name="compose_save_draft">කටුපිටපත සුරකින්නද\?</string> + <string name="post_sensitive_media_title">සංවේදී අන්තර්ගතයකි</string> + <string name="profile_badge_bot_text">ස්වයංක්රමලේඛය</string> + <string name="action_bookmark">පොත්යොමුව</string> + <string name="caption_notoemoji">ගූගල් හි වත්මන් ඉමෝජි කට්ටලය</string> + <string name="app_theme_auto">ස්වයංක්රීව ඉර බැසීමේදී</string> + <string name="failed_report">වාර්තා කිරීමට අසමත් විය</string> + <string name="abbreviated_years_ago">අවු. %1$d</string> + <string name="action_open_as">%1$s ලෙස විවෘත කරන්න</string> + <string name="action_open_faved_by">ප්රියතමයන් පෙන්වන්න</string> + <string name="abbreviated_in_minutes">වි. %1$d කින්</string> + <plurals name="poll_info_votes"> + <item quantity="one">ජන්ද %1$s</item> + <item quantity="other">ජන්ද %1$s</item> + </plurals> + <string name="action_more">තව</string> + <string name="action_view_bookmarks">පොත්යොමු</string> + <string name="notification_mention_format">%1$s ඔබව සඳහන් කළා</string> + <string name="compose_active_account_description">%1$s ගිණුම සමඟ පළකරන්න</string> + <string name="hint_additional_info">අතිරේක අදහස්</string> + <string name="notification_favourite_name">ප්රියතමයන්</string> + <string name="download_fonts">ඔබ මේ ඉමෝජි කට්ටල පළමුව බාගත යුතුයි</string> + <string name="description_visibility_direct">සෘජු</string> + <string name="hint_search">සොයන්න…</string> + <string name="pref_title_app_theme">යෙදුමේ තේමාව</string> + <string name="title_bookmarks">පොත්යොමු</string> + <string name="dialog_mute_warning">\@%1$s නිහඬ\?</string> + <string name="compose_shortcut_short_label">රචනා කරන්න</string> + <string name="poll_ended_voted">ඔබ ජන්දය දුන් මත විමසුව නිම වී ඇත</string> + <string name="about_title_activity">පිළිබඳව</string> + <string name="mute_domain_warning_dialog_ok">සමස්ථ වසම සඟවන්න</string> + <string name="profile_metadata_label">පැතිකඩ පාරදත්ත</string> + <string name="action_copy_link">සබැඳිය පිටපත්</string> + <string name="title_direct_messages">සෘජු පණිවිඩ</string> + <string name="downloading_media">මාධ්ය බාගත වෙමින්</string> + <string name="pref_title_show_media_preview">මාධ්ය පෙරදසුන් බාගන්න</string> + <string name="load_more_placeholder_text">තව පූරණය</string> + <string name="dialog_download_image">බාගන්න</string> + <string name="title_mentions_dialog">සඳැහුම්</string> + <string name="action_reply">පිළිතුර</string> + <string name="pref_title_edit_notification_settings">දැනුම්දීම්</string> + <string name="confirmation_unblocked">පරිශීලක අනවහිර කෙරිණි</string> + <string name="profile_metadata_add">දත්ත එක්කරන්න</string> + <string name="send_post_content_to">වෙත ටූට් බෙදාගන්න…</string> + <string name="send_post_notification_cancel_title">යැවීම අවලංගු කෙරිණි</string> + <string name="action_unblock">අනවහිර</string> + <string name="action_send_public">ටූට්!</string> + <string name="title_favourites">ප්රියතමයන්</string> + <string name="download_image">%1$s බාගත වෙමින්</string> + <string name="failed_fetch_posts">තත්ව ගෙන ඒමට අසමත් විය</string> + <string name="description_post_media">මාධ්ය: %1$s</string> + <string name="action_mute_conversation">සංවාදය නිහඬ කරන්න</string> + <string name="send_post_notification_channel_name">ටූට්ස් යැවෙමින්</string> + <string name="pref_title_thread_filter_keywords">සංවාද</string> + <string name="post_text_size_large">විශාල</string> + <string name="description_visibility_public">ප්රසිද්ධ</string> + <string name="add_account_description">නව මාස්ටඩන් ගිණුමක් එක්කරන්න</string> + <string name="dialog_title_finishing_media_upload">මාධ්ය උඩුගත වීම අහවර වෙමින්</string> + <plurals name="poll_timespan_seconds"> + <item quantity="one">තත්. %1$d ක් ඉතිරිය</item> + <item quantity="other">තත්. %1$d ක් ඉතිරිය</item> + </plurals> + <string name="action_logout_confirm">ඔබට %1$s ගිණුමෙන් නික්මෙන්ට ඇවැසි බව විශ්වාසද\?</string> + <string name="title_public_local">ස්ථානීය</string> + <string name="action_view_favourites">ප්රියතමයන්</string> + <string name="label_quick_reply">පිළිතුරු…</string> + <string name="limit_notifications">කාලරේඛා දැනුම්දීම් සීමාකරන්න</string> + <string name="send_post_notification_error_title">ටූට් යැවීමේ දෝෂයකි</string> + <string name="filter_addition_title">පෙරහන එකතු කරන්න</string> + <string name="pref_default_media_sensitivity">සැමවිටම මාධ්ය සංවේදී ලෙස සලකුණු කරන්න</string> + <string name="restart_required">යෙදුම යළි ඇරඹීම ඇවැසිය</string> + <string name="restart">යළි අරඹන්න</string> + <string name="app_theme_black">කළු</string> + <string name="abbreviated_in_years">වර්. %1$d කින්</string> + <string name="restart_emoji">මෙම වෙනස්කම් යෙදීමට ඔබ ටුස්කි නැවත ඇරඹිය යුතුය</string> + <string name="action_edit">සංස්කරණය</string> + <string name="button_continue">ඉදිරියට</string> + <string name="pref_title_post_filter">කාලරේඛාව පෙරීම</string> + <string name="conversation_more_recipients">%1$s, %2$s සහ තවත් %3$d</string> + <string name="pref_title_timelines">කාලරේඛා</string> + <string name="action_mute_domain">%1$s නිහඬ කරන්න</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="replying_to">\@%1$s වෙත පිළිතුරු දෙමින්</string> + <string name="action_block">අවහිර</string> + <string name="download_media">මාධ්ය බාගන්න</string> + <string name="action_view_domain_mutes">සැඟවුනු වසම්</string> + <string name="post_text_size_small">කුඩා</string> + <string name="pref_title_alway_open_spoiler">අන්තර්ගත අවවාද සමඟ ඇති ටූට්ස් සැමවිටම විහිදන්න</string> + <string name="action_undo">පෙරසේ</string> + <string name="post_content_show_more">විහිදන්න</string> + <string name="action_mute">නිහඬ කරන්න</string> + <string name="about_tusky_version">ටුස්කි %1$s</string> + <string name="about_project_site">වියමන අඩවිය: +\n https://tusky.app</string> + <string name="action_accept">පිළිගන්න</string> + <string name="abbreviated_in_hours">පැ. %1$d කින්</string> + <string name="create_poll_title">මතවිමසුම</string> + <string name="action_remove">ඉවත් කරන්න</string> + <string name="action_add_media">මාධ්ය එකතු කරන්න</string> + <string name="abbreviated_in_days">ද. %1$d කින්</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s සහ වෙනත් %4$d</string> + <string name="action_open_post">ටූට් විවෘත කරන්න</string> + <string name="title_domain_mutes">සැඟවුනු වසම්</string> + <string name="abbreviated_hours_ago">පැය %1$d</string> + <string name="pref_failed_to_sync">සැකසුම් සමමුහූර්ත වීමට අසමත් විය</string> + <string name="pref_title_show_replies">පිළිතුරු පෙන්වන්න</string> + <string name="action_view_profile">පැතිකඩ</string> + <string name="caption_systememoji">ඔබගේ උපාංගයේ පෙරනිමි ඉමෝජි කට්ටලය</string> + <string name="notification_mention_descriptions">නව සැඳහුම් පිළිබඳව දැනුම්දීම්</string> + <string name="action_search">සොයන්න</string> + <string name="action_share_as">ලෙස බෙදාගන්න …</string> + <string name="title_home">මුල</string> + <string name="abbreviated_minutes_ago">වි. %1$d</string> + <string name="failed_search">සෙවීමට අසමත් විය</string> + <string name="no_announcements">නිවේදන නැත.</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="action_save">සුරකින්න</string> + <string name="title_mutes">නිහඬ කළ පරිශීලකයින්</string> + <string name="no_drafts">ඔබ සතුව කටුපිටපත් නැත.</string> + <string name="title_view_thread">ටූට්</string> + <string name="pref_title_timeline_filters">පෙරහන්</string> + <string name="report_sent_success">\@%1$s වෙත සාර්ථකව වාර්තා කෙරිණි</string> + <string name="download_failed">බාගැනීමට අසමත් විය</string> + <string name="pref_title_language">භාෂාව</string> + <string name="account_note_saved">සුරැකිණි!</string> + <string name="about_tusky_account">ටුස්කි\'හි පැතිකඩ</string> + <string name="post_share_content">ටූට්හි අන්තර්ගතය බෙදාගන්න</string> + <string name="profile_metadata_content_label">අන්තර්ගතය</string> + <string name="notification_subscription_name">නව ටූට්ස්</string> + <string name="action_open_media_n">#%1$d මාධ්ය විවෘත කරන්න</string> + <string name="action_view_media">මාධ්ය</string> + <plurals name="poll_info_people"> + <item quantity="one">පුද්ගලයින් %1$s</item> + <item quantity="other">මිනිසුන් %1$s</item> + </plurals> + <string name="send_media_to">වෙත මාධ්ය බෙදාගන්න…</string> + <string name="pref_title_public_filter_keywords">ප්රසිද්ධ කාලරේඛා</string> + <string name="post_text_size_smallest">කුඩාම</string> + <string name="notification_poll_name">මත විමසුම්</string> + <string name="post_sensitive_media_directions">දැකීමට ඔබන්න</string> + <string name="poll_info_closed">වසා ඇත</string> + <plurals name="poll_timespan_days"> + <item quantity="one">දවස් %1$d ක් ඉතිරිය</item> + <item quantity="other">දවස් %1$d ක් ඉතිරිය</item> + </plurals> + <string name="pref_title_notification_alert_sound">ශබ්දය සමඟ දන්වන්න</string> + <string name="about_powered_by_tusky">ටුස්කි මගින් බලගන්වා ඇත</string> + <string name="pref_title_alway_show_sensitive_media">සැමවිටම සංවේදී අන්තර්ගත පෙන්වන්න</string> + <string name="title_announcements">නිවේදන</string> + <plurals name="poll_timespan_minutes"> + <item quantity="one">විනාඩි %1$d ක් ඉතිරිය</item> + <item quantity="other">විනාඩි %1$d ක් ඉතිරිය</item> + </plurals> + <string name="action_access_drafts">කටුපිටපත්</string> + <string name="poll_ended_created">ඔබ සෑදූ මත විමසුම නිම වී ඇත</string> + <string name="pref_title_bot_overlay">ස්වයංක්රමලේඛ සඳහා දර්ශකය පෙන්වන්න</string> + <plurals name="favs"> + <item quantity="one">ප්රියතමයන් <b>%1$s</b></item> + <item quantity="other">ප්රියතමයන් <b>%1$s</b></item> + </plurals> + <string name="action_favourite">ප්රියතම</string> + <string name="action_mentions">සඳැහුම්</string> + <string name="filter_dialog_update_button">යාවත්කාල</string> + <string name="notification_summary_small">%1$s සහ %2$s</string> + <string name="about_tusky_license">ටුස්කි යනු නොමිලේ සහ විවෘත-මූලාශ්ර මෘදුකාංගයකි. එය ජීඑන්යූ පොදු බලපත්ර අනුවාදය 3 යටතේ අවසර ලබා ඇත. ඔබට මෙතැනින් බලපත්රය දැකීමට හැකිය: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="title_links_dialog">සබැඳි</string> + <string name="title_media">මාධ්ය</string> + <string name="dialog_block_warning">\@%1$s අවහිර\?</string> + <string name="pref_title_notification_alert_light">ආලෝකය සමඟ දන්වන්න</string> + <string name="account_note_hint">මෙම ගිණුම පිළිබඳව ඔබගේ පෞද්ගලික සටහන</string> + <string name="action_close">වසන්න</string> + <string name="title_notifications">දැනුම්දීම්</string> + <string name="post_username_format">\@%1$s</string> + <string name="action_login">මාස්ටඩන් සමඟ පිවිසෙන්න</string> + <string name="poll_vote">ජන්දය</string> + <string name="filter_dialog_whole_word">මුළු වචනය</string> + <string name="drafts_post_failed_to_send">මෙම ටූට් යැවීමට අසමත් විය!</string> + <string name="post_media_video">දෘශ්යකය</string> + <string name="later">පසුව</string> + <string name="action_edit_own_profile">සංස්කරණය</string> + <string name="app_them_dark">අඳුරු</string> + <string name="message_empty">කිසිවක් නැත.</string> + <string name="send_post_notification_title">ටූට් යැවෙමින්…</string> + <string name="system_default">පද්ධති පෙරනිමිය</string> + <string name="action_mention">සඳැහුම</string> + <string name="filter_dialog_remove_button">ඉවත් කරන්න</string> + <string name="dialog_mute_hide_notifications">දැනුම්දීම් සඟවන්න</string> + <string name="pref_post_text_size">තත්ව පාඨයේ ප්රමාණය</string> + <string name="pref_title_show_cards_in_timelines">කාලරේඛාවෙහි සබැඳි පෙරදසුන් පෙන්වන්න</string> + <string name="action_links">සබැඳි</string> + <string name="pref_title_browser_settings">අතිරික්සුව</string> + <string name="abbreviated_seconds_ago">තත්. %1$d</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml new file mode 100644 index 0000000..aa9947e --- /dev/null +++ b/app/src/main/res/values-sk/strings.xml @@ -0,0 +1,130 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="action_login">Prihlásiť sa účtom Mastodon</string> + <string name="title_drafts">Koncepty</string> + <string name="action_logout">Odhlásiť sa</string> + <string name="action_view_preferences">Nastavenia</string> + <string name="action_view_account_preferences">Nastavenia účtu</string> + <string name="action_edit_profile">Upraviť profil</string> + <string name="action_search">Hľadať</string> + <string name="action_lists">Zoznamy</string> + <string name="title_lists">Zoznamy</string> + <string name="error_generic">Vyskytla sa chyba.</string> + <string name="title_notifications">Oznámenia</string> + <string name="title_direct_messages">Priame správy</string> + <string name="title_posts_with_replies">S odpoveďmi</string> + <string name="title_posts_pinned">Pripnuté</string> + <string name="title_followers">Sledujúci</string> + <string name="title_bookmarks">Záložky</string> + <string name="title_licenses">Licencie</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_sensitive_media_title">Citlivý obsah</string> + <string name="post_sensitive_media_directions">Kliknite pre zobrazenie</string> + <string name="post_content_warning_show_more">Zobraziť viac</string> + <string name="post_content_warning_show_less">Zobraziť menej</string> + <string name="post_content_show_more">Rozbaliť</string> + <string name="post_content_show_less">Zabaliť</string> + <string name="report_username_format">Nahlásiť používateľa @%1$s</string> + <string name="action_quick_reply">Rýchla odpoveď</string> + <string name="action_reply">Odpovedať</string> + <string name="link_whats_an_instance">Čo je server\?</string> + <string name="title_favourites">Obľúbené</string> + <string name="about_title_activity">O aplikácii</string> + <string name="error_network">Vyskytla sa chyba v sieti! Prosím skontrolujte svoje pripojenie a skúste to znova!</string> + <string name="error_empty">Toto nemôže byť prázdne.</string> + <string name="error_authorization_denied">Autorizácia bola zamietnutá.</string> + <string name="error_retrieving_oauth_token">Nepodarilo sa získať prihlasovací token.</string> + <string name="error_media_upload_opening">Súbor sa nepodarilo otvoriť.</string> + <string name="title_home">Domov</string> + <string name="title_tab_preferences">Panely</string> + <string name="action_bookmark">Vytvoriť záložku</string> + <string name="action_follow">Sledovať</string> + <string name="action_unfollow">Prestať sledovať</string> + <string name="action_block">Blokovať</string> + <string name="action_unblock">Odblokovať</string> + <string name="action_report">Nahlásiť</string> + <string name="action_edit">Upraviť</string> + <string name="action_delete">Vymazať</string> + <string name="action_delete_and_redraft">Vymazať a prepísať</string> + <string name="action_reset_schedule">Obnoviť</string> + <string name="action_links">Odkazy</string> + <string name="action_mentions">Zmienky</string> + <string name="action_hashtags">Hashtagy</string> + <string name="action_open_faved_by">Zobraziť obľúbené</string> + <string name="title_hashtags_dialog">Hashtagy</string> + <string name="title_mentions_dialog">Zmienky</string> + <string name="title_links_dialog">Odkazy</string> + <string name="action_open_media_n">Otvoriť médium #%1$d</string> + <string name="download_image">Sťahovanie %1$s</string> + <string name="action_copy_link">Kopírovať odkaz</string> + <string name="action_open_as">Otvoriť ako %1$s</string> + <string name="confirmation_reported">Odoslané!</string> + <string name="confirmation_unblocked">Používateľ bol odblokovaný</string> + <string name="hint_domain">Ktorý server\?</string> + <string name="hint_display_name">Zobrazované meno</string> + <string name="hint_note">O vás</string> + <string name="hint_search">Hľadať…</string> + <string name="search_no_results">Žiadne výsledky</string> + <string name="label_quick_reply">Odpovedať…</string> + <string name="dialog_message_uploading_media">Nahrávanie…</string> + <string name="dialog_download_image">Stiahnuť</string> + <string name="error_invalid_domain">Neplatná doména</string> + <string name="error_failed_app_registration">Autentizácia servru zlyhala.</string> + <string name="error_no_web_browser_found">Nepodarilo sa nájsť použiteľný webový prehliadač.</string> + <string name="error_authorization_unknown">Vyskytla sa neidentifikovaná chyba autorizácie.</string> + <string name="error_compose_character_limit">Príspevok je príliš dlhý!</string> + <string name="error_media_upload_type">Tento typ súboru nemôže byť nahraný.</string> + <string name="error_sender_account_gone">Chyba pri odosielaní tootu.</string> + <string name="title_view_thread">Toot</string> + <string name="notification_favourite_format">%1$s si obľúbil/a váš toot</string> + <string name="action_send">TOOT</string> + <string name="action_send_public">TOOT!</string> + <string name="action_toggle_visibility">Viditeľnosť tootu</string> + <string name="action_schedule_post">Naplánovať toot</string> + <string name="dialog_delete_post_warning">Vymazať tento toot\?</string> + <string name="dialog_redraft_post_warning">Vymazať a prepísať tento toot\?</string> + <string name="post_share_content">Zdieľať obsah tootu</string> + <string name="post_share_link">Zdieľať odkaz tootu</string> + <string name="send_post_notification_title">Odosielanie tootu…</string> + <string name="send_post_notification_error_title">Chyba pri odosielaní tootu</string> + <string name="send_post_notification_saved_content">Kópia vášho tootu bola uložená do konceptov</string> + <string name="action_open_post">Otvoriť toot</string> + <string name="compose_shortcut_long_label">Napísať toot</string> + <string name="title_scheduled_posts">Plánované tooty</string> + <string name="action_access_scheduled_posts">Plánované tooty</string> + <string name="label_avatar">Avatar</string> + <string name="action_remove">Odstrániť</string> + <string name="lock_account_label">Uzamknúť účet</string> + <string name="compose_save_draft">Uložiť koncept\?</string> + <string name="send_post_notification_channel_name">Odosielanie tootov</string> + <string name="send_post_notification_cancel_title">Odosielanie bolo zrušené</string> + <string name="performing_lookup_title">Vyhľadávanie…</string> + <string name="later">Neskôr</string> + <string name="restart">Reštartovať</string> + <string name="profile_badge_bot_text">Robot</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Metadáta profilu</string> + <string name="profile_metadata_add">pridať dáta</string> + <string name="profile_metadata_content_label">Obsah</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s a %2$s</string> + <string name="description_post_media_no_description_placeholder">Žiadny popis</string> + <string name="description_visibility_public">Verejný</string> + <string name="action_reblog">Podporiť</string> + <string name="action_unreblog">Prestať podporovať</string> + <string name="description_visibility_private">Sledujúci</string> + <string name="post_privacy_public">Verejný</string> + <string name="action_view_bookmarks">Záložky</string> + <string name="notification_summary_small">%1$s a %2$s</string> + <string name="edit_poll">Upraviť</string> + <string name="hashtags">Hashtagy</string> + <string name="action_access_drafts">Koncepty</string> + <string name="action_edit_own_profile">Upraviť</string> + <string name="pref_title_notifications_enabled">Oznámenia</string> + <string name="pref_title_edit_notification_settings">Oznámenia</string> + <string name="action_view_favourites">Obľúbené</string> + <string name="filter_dialog_remove_button">Odstrániť</string> + <string name="notification_favourite_name">Obľúbené</string> + <string name="pref_title_post_tabs">Panely</string> +</resources> diff --git a/app/src/main/res/values-sl/strings.xml b/app/src/main/res/values-sl/strings.xml new file mode 100644 index 0000000..1336a1d --- /dev/null +++ b/app/src/main/res/values-sl/strings.xml @@ -0,0 +1,448 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Pojavila se je napaka.</string> + <string name="error_network">Prišlo je do omrežne napake. Preverite povezavo in poskusite znova.</string> + <string name="error_empty">To ne sme biti prazno.</string> + <string name="error_invalid_domain">Vnesena je neveljavna domena</string> + <string name="error_no_web_browser_found">Spletnega brskalnika ni bilo mogoče najti.</string> + <string name="error_authorization_denied">Avtorizacija je bila zavrnjena. Če ste prepričani, da ste uporabili pravilne podatke, se poskusite prijaviti v brskalnik iz menija.</string> + <string name="error_retrieving_oauth_token">Ni bilo mogoče pridobiti žetona za prijavo. Če se to nadaljuje, se poskusite prijaviti z brskalnikom iz menija.</string> + <string name="error_compose_character_limit">Predolgo sporočilo!</string> + <string name="error_media_upload_type">Te vrste datoteke ni mogoče poslati.</string> + <string name="error_media_upload_opening">Te datoteke ni bilo mogoče odpreti.</string> + <string name="error_media_upload_permission">Potrebno je dovoljenje za branje medijev.</string> + <string name="error_media_download_permission">Potrebno je dovoljenje za shranjevanje medijev.</string> + <string name="error_media_upload_image_or_video">Sporočilo ne more vsebovati slik in videoposnetkov hkrati.</string> + <string name="error_media_upload_sending">Prenos ni uspel.</string> + <string name="error_sender_account_gone">Napaka pri pošiljanju sporočila.</string> + <string name="title_home">Domov</string> + <string name="title_notifications">Obvestila</string> + <string name="title_public_local">Lokalno</string> + <string name="title_public_federated">Združeno</string> + <string name="title_direct_messages">Neposredna Sporočila</string> + <string name="title_tab_preferences">Zavihki</string> + <string name="title_view_thread">Tut</string> + <string name="title_posts">Objave</string> + <string name="title_posts_with_replies">Z odgovori</string> + <string name="title_posts_pinned">Pripeto</string> + <string name="title_follows">Sledi</string> + <string name="title_followers">Sledilci</string> + <string name="title_favourites">Priljubljene</string> + <string name="title_mutes">Utišani uporabniki</string> + <string name="title_blocks">Blokirani uporabniki</string> + <string name="title_follow_requests">Zahteve za Sledenje</string> + <string name="title_edit_profile">Uredi svoj profil</string> + <string name="title_drafts">Osnutki</string> + <string name="title_licenses">Licence</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_sensitive_media_title">Občutljiva vsebina</string> + <string name="post_media_hidden_title">Medij je skrit</string> + <string name="post_sensitive_media_directions">Kliknite za ogled</string> + <string name="post_content_warning_show_more">Pokaži več</string> + <string name="post_content_warning_show_less">Pokaži manj</string> + <string name="post_content_show_more">Razširi</string> + <string name="post_content_show_less">Strni</string> + <string name="message_empty">Tukaj ni ničesar.</string> + <string name="footer_empty">Tukaj ni ničesar. Potegnite navzdol za osvežitev!</string> + <string name="notification_reblog_format">%1$s je spodbudil tvoj tut</string> + <string name="notification_favourite_format">%1$s je vzljubil vaš tut</string> + <string name="notification_follow_format">%1$s vam sledi</string> + <string name="report_username_format">Prijavi @%1$s</string> + <string name="report_comment_hint">Dodatni komentarji\?</string> + <string name="action_quick_reply">Hiter odgovor</string> + <string name="action_reply">Odgovori</string> + <string name="action_reblog">Spodbudi</string> + <string name="action_unreblog">Odstrani spodbudo</string> + <string name="action_favourite">Vzljubi</string> + <string name="action_more">Več</string> + <string name="action_compose">Sestavi</string> + <string name="action_login">Prijavite se z Mastodonom</string> + <string name="action_logout">Odjava</string> + <string name="action_logout_confirm">Ali ste prepričani, da se želite odjaviti iz računa %1$s\? To bo izbrisalo vse lokalne podatke o računu, vključno z osnutki in preferencami.</string> + <string name="action_follow">Sledi</string> + <string name="action_unfollow">Prenehaj slediti</string> + <string name="action_block">Blokiraj</string> + <string name="action_unblock">Odblokiraj</string> + <string name="action_hide_reblogs">Skirj spodbude</string> + <string name="action_show_reblogs">Prikaži spodbude</string> + <string name="action_report">Prijavi</string> + <string name="action_delete">Izbriši</string> + <string name="action_send">TUTNI</string> + <string name="action_send_public">TUTNI!</string> + <string name="action_retry">Poskusi znova</string> + <string name="action_close">Zapri</string> + <string name="action_view_profile">Profil</string> + <string name="action_view_preferences">Možnosti</string> + <string name="action_view_account_preferences">Možnosti računa</string> + <string name="action_view_favourites">Priljubljeni</string> + <string name="action_view_mutes">Utišani uporabniki</string> + <string name="action_view_blocks">Blokirani uporabniki</string> + <string name="action_view_follow_requests">Prošnje za sledenje</string> + <string name="action_view_media">Mediji</string> + <string name="action_open_in_web">Odpri v brskalniku</string> + <string name="action_add_media">Dodaj medij</string> + <string name="action_photo_take">Posnemi fotografijo</string> + <string name="action_share">Deli</string> + <string name="action_mute">Utišaj</string> + <string name="action_mention">Omeni</string> + <string name="action_hide_media">Skrij medij</string> + <string name="action_open_drawer">Odpri predal</string> + <string name="action_save">Shrani</string> + <string name="action_edit_profile">Uredi profil</string> + <string name="action_edit_own_profile">Uredi</string> + <string name="action_undo">Razveljavi</string> + <string name="action_accept">Sprejmi</string> + <string name="action_reject">Zavrni</string> + <string name="action_search">Iskanje</string> + <string name="action_access_drafts">Osnutki</string> + <string name="action_toggle_visibility">Vidljivost tuta</string> + <string name="action_content_warning">Opozorilo o vsebini</string> + <string name="action_emoji_keyboard">Tipkovnica z emotikoni</string> + <string name="action_add_tab">Dodaj zavihek</string> + <string name="action_links">Povezave</string> + <string name="action_mentions">Omembe</string> + <string name="action_open_reblogger">Odpri spodbujenega avtorja</string> + <string name="action_open_reblogged_by">Prikaži spodbude</string> + <string name="action_open_faved_by">Prikaži priljubljene</string> + <string name="title_mentions_dialog">Omembe</string> + <string name="title_links_dialog">Povezave</string> + <string name="action_open_media_n">Odpri medij #%1$d</string> + <string name="download_image">Prejemanje %1$s</string> + <string name="action_copy_link">Kopiraj povezavo</string> + <string name="action_open_as">Odpri kot %1$s</string> + <string name="action_share_as">Deli kot …</string> + <string name="download_media">Prenos medija</string> + <string name="downloading_media">Prejemanje medija</string> + <string name="send_post_link_to">Deli URL tuta z…</string> + <string name="send_post_content_to">Deli tut z…</string> + <string name="send_media_to">Deli medij z…</string> + <string name="confirmation_reported">Pošlji!</string> + <string name="hint_compose">Kaj se dogaja\?</string> + <string name="hint_content_warning">Opozorilo o vsebini</string> + <string name="hint_display_name">Prikazano ime</string> + <string name="hint_note">Bio</string> + <string name="hint_search">Iskanje…</string> + <string name="search_no_results">Ni rezultatov</string> + <string name="label_quick_reply">Odgovori…</string> + <string name="label_avatar">Podoba</string> + <string name="label_header">Glava</string> + <string name="login_connection">Povezovanje…</string> + <string name="error_failed_app_registration">Overitev s to instanco ni uspela. Če se to ponovi, se poskusite prijaviti v brskalnik iz menija.</string> + <string name="error_authorization_unknown">Prišlo je do neznane napake pri pooblastitvi. Če se to ponovi, se poskusite prijaviti v brskalniku iz menija.</string> + <string name="action_unfavourite">Odstrani priljubljene</string> + <string name="action_unmute">Prekini utišanje</string> + <string name="action_hashtags">Ključniki</string> + <string name="title_hashtags_dialog">Ključniki</string> + <string name="confirmation_unblocked">Odblokiran uporabnik</string> + <string name="confirmation_unmuted">Prekinjeno utišanje uporabnika</string> + <string name="hint_domain">Katero vozlišče\?</string> + <string name="link_whats_an_instance">Kaj je instanca\?</string> + <string name="dialog_whats_an_instance">Tu lahko vnesete naslov ali domeno katerega koli vozlišča, na primer mastodon.social, icosahedron.website, social.tchncs.de in <a href="https://instances.social"> več! </a> +\n +\nČe še nimate računa, lahko vnesete ime vozlišča, kateremu bi se radi pridružili, in tam ustvarite račun. +\n +\nVozlišče je ena lokacija, kjer je gostovanje vašega računa, vendar lahko preprosto komunicirate in sledite ljudem na drugih vozliščih, kot da bi bili na isti lokaciji. +\n +\nVeč informacij najdete na naslovu <a href="https://joinmastodon.org">joinmastodon.org</a>. </string> + <string name="dialog_title_finishing_media_upload">Dokončanje pošiljanja medija</string> + <string name="dialog_message_uploading_media">Pošiljanje…</string> + <string name="dialog_download_image">Prejmi</string> + <string name="dialog_message_cancel_follow_request">Želite preklicati to zahtevo\?</string> + <string name="dialog_unfollow_warning">Prenehajte slediti temu računu\?</string> + <string name="dialog_delete_post_warning">Želite izbrisati ta tut\?</string> + <string name="visibility_public">Javno: Objavi v javnih časovnicah</string> + <string name="visibility_unlisted">Ni prikazano: Ne prikaže v javnih časovnicah</string> + <string name="visibility_private">Samo sledilci: Objavi samo sledilcem</string> + <string name="visibility_direct">Neposredno: Objavi samo pri omenjenih uporabnikih</string> + <string name="pref_title_edit_notification_settings">Obvestila</string> + <string name="pref_title_notifications_enabled">Obvestila</string> + <string name="pref_title_notification_alerts">Opozorila</string> + <string name="pref_title_notification_alert_sound">Obvesti z zvokom</string> + <string name="pref_title_notification_alert_vibrate">Obvesti z vibracijo</string> + <string name="pref_title_notification_alert_light">Obvesti s svetlobo</string> + <string name="pref_title_notification_filters">Obvesti me, ko</string> + <string name="pref_title_notification_filter_mentions">sem omenjen</string> + <string name="pref_title_notification_filter_follows">me kdo sledi</string> + <string name="pref_title_notification_filter_reblogs">so moje objave spodbujene</string> + <string name="pref_title_notification_filter_favourites">so moje objave vzljubljene</string> + <string name="pref_title_appearance_settings">Videz</string> + <string name="pref_title_app_theme">Tema aplikacije</string> + <string name="pref_title_timelines">Časovnice</string> + <string name="pref_title_timeline_filters">Filtri</string> + <string name="pref_title_browser_settings">Brskalnik</string> + <string name="pref_title_custom_tabs">Uporabi Chromove zavihke po meri</string> + <string name="pref_title_language">Jezik</string> + <string name="pref_title_post_filter">Filtriranje časovnice</string> + <string name="pref_title_post_tabs">Zavihki</string> + <string name="pref_title_show_boosts">Pokaži spodbude</string> + <string name="pref_title_show_replies">Pokaži odgovore</string> + <string name="pref_title_show_media_preview">Prenesi predoglede medijev</string> + <string name="pref_title_proxy_settings">Posredniški strežnik</string> + <string name="pref_title_http_proxy_settings">Pos. strž. HTTP</string> + <string name="pref_title_http_proxy_enable">Omogoči pos. strž. HTTP</string> + <string name="pref_title_http_proxy_server">Pos. strž HTTP</string> + <string name="pref_title_http_proxy_port">Vrata pos. strž. HTTP</string> + <string name="pref_default_post_privacy">Privzeta zasebnost objave</string> + <string name="pref_default_media_sensitivity">Vedno označite medije kot občutljive</string> + <string name="pref_publishing">Objavljanje (sinhronizirano s strežnikom)</string> + <string name="pref_failed_to_sync">Nastavitev ni bilo mogoče sinhronizirati</string> + <string name="pref_post_text_size">Velikost besedila statusa</string> + <string name="notification_mention_name">Nove omembe</string> + <string name="notification_mention_descriptions">Obvestila o novih omembah</string> + <string name="notification_follow_name">Novi sledilci</string> + <string name="notification_follow_description">Obvestila o novih sledilcih</string> + <string name="notification_boost_name">Spodbude</string> + <string name="notification_boost_description">Obvestila, ko so vaši tuti spodbujeni</string> + <string name="notification_favourite_name">Priljubljene</string> + <string name="notification_favourite_description">Obvestila, ko so vaši tuti vzljubljeni</string> + <string name="notification_mention_format">%1$s vas je omenil-a</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s in %4$d ostali</string> + <string name="notification_summary_medium">%1$s, %2$s in %3$s</string> + <string name="notification_summary_small">%1$s in %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="other">%1$d novih interakcij</item> + </plurals> + <string name="description_account_locked">Zaklenjen račun</string> + <string name="about_title_activity">O aplikaciji</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tusky je prosta in odprtokodna programska oprema. Licencirana je pod licenco GNU General Public License različice 3. Licenco si lahko ogledate tukaj: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="about_project_site">Spletna stran projekta: +\nhttps://tusky.app</string> + <string name="about_bug_feature_request_site">Poročila o napakah in želje za nove funkcije: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Profil Tusky</string> + <string name="post_share_content">Deli vsebino tuta</string> + <string name="post_share_link">Deli povezavo do tuta</string> + <string name="post_media_images">Slike</string> + <string name="post_media_video">Video</string> + <string name="state_follow_requested">Prošnja za sledenje</string> + <string name="abbreviated_in_years">v %1$dy</string> + <string name="abbreviated_in_days">v %1$dd</string> + <string name="abbreviated_in_hours">v %1$dh</string> + <string name="abbreviated_in_minutes">in %1$dm</string> + <string name="abbreviated_in_seconds">v %1$ds</string> + <string name="abbreviated_years_ago">%1$dy</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="follows_you">Vam sledi</string> + <string name="pref_title_alway_show_sensitive_media">Vedno prikaži občutljivo vsebino</string> + <string name="title_media">Mediji</string> + <string name="replying_to">Odgovori @%1$s</string> + <string name="load_more_placeholder_text">naloži več</string> + <string name="pref_title_public_filter_keywords">Javne časovnice</string> + <string name="pref_title_thread_filter_keywords">Pogovori</string> + <string name="filter_addition_title">Dodaj filter</string> + <string name="filter_edit_title">Uredi filter</string> + <string name="filter_dialog_remove_button">Odstrani</string> + <string name="filter_dialog_update_button">Posodobi</string> + <string name="filter_add_description">Filtriraj frazo</string> + <string name="add_account_name">Dodaj račun</string> + <string name="add_account_description">Dodaj nov Mastodon račun</string> + <string name="action_lists">Seznami</string> + <string name="title_lists">Seznami</string> + <string name="error_create_list">Seznama ni bilo mogoče ustvariti</string> + <string name="error_rename_list">Seznama ni bilo mogoče preimenovati</string> + <string name="error_delete_list">Seznama ni bilo mogoče izbrisati</string> + <string name="action_create_list">Ustvari seznam</string> + <string name="action_rename_list">Preimenuj seznam</string> + <string name="action_delete_list">Izbriši seznam</string> + <string name="hint_search_people_list">Poiščite osebe, katerim sledite</string> + <string name="action_add_to_list">Dodaj račun na seznam</string> + <string name="action_remove_from_list">Odstrani račun iz seznama</string> + <string name="compose_active_account_description">Objavljanje z računom %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">Opišite za slabovidne +\n(omejitev znakov - %1$d)</item> + </plurals> + <string name="action_set_caption">Nastavi opis</string> + <string name="action_remove">Odstrani</string> + <string name="lock_account_label">Zakleni račun</string> + <string name="lock_account_label_description">Zahtevana je ročna potrditev sledilcev</string> + <string name="compose_save_draft">Shrani osnutek\?</string> + <string name="send_post_notification_title">Pošiljanje tuta…</string> + <string name="send_post_notification_error_title">napaka pri pošiljanju tuta</string> + <string name="send_post_notification_channel_name">Pošiljanje tutov</string> + <string name="send_post_notification_cancel_title">Pošiljanje je preklicano</string> + <string name="send_post_notification_saved_content">Kopija tuta je bila shranjena v osnutke</string> + <string name="action_compose_shortcut">Sestavi</string> + <string name="error_no_custom_emojis">Vaše vozlišče %1$s nima emotikonov po meri</string> + <string name="emoji_style">Slog emotikonov</string> + <string name="system_default">Privzete nastavitve sistema</string> + <string name="download_fonts">Najprej boste morali prenesti te emotikone</string> + <string name="performing_lookup_title">Izvajanje iskanja…</string> + <string name="expand_collapse_all_posts">Razširi/Strni vse statuse</string> + <string name="action_open_post">Odpri tut</string> + <string name="restart_required">Potreben je ponovni zagon aplikacije</string> + <string name="restart_emoji">Če želite uveljaviti te spremembe, morate znova zagnati Tusky</string> + <string name="later">Kasneje</string> + <string name="restart">Znova zaženi</string> + <string name="caption_systememoji">Privzeti komplet emotikonov vaše naprave</string> + <string name="caption_blobmoji">Blob emotikoni so znani od Android 4.4-7.1</string> + <string name="caption_twemoji">Mastodonov privzeti komplet emotikonov</string> + <string name="download_failed">Prenos ni uspel</string> + <string name="profile_badge_bot_text">Robot</string> + <string name="account_moved_description">%1$s se je prestavil/a na:</string> + <string name="reblog_private">Spodbudi izvirnemu občinstvu</string> + <string name="unreblog_private">Prekini spodbudo</string> + <string name="license_description">Tusky vsebuje kodo in sredstva iz naslednjih odprtokodnih projektov:</string> + <string name="license_apache_2">Licencirano pod licenco Apache (spodaj)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Metapodatki profila</string> + <string name="profile_metadata_add">dodaj podatke</string> + <string name="profile_metadata_label_label">Oznaka</string> + <string name="profile_metadata_content_label">Vsebina</string> + <string name="pref_title_absolute_time">Uporabite absolutni čas</string> + <string name="label_remote_account">Spodnje informacije lahko nepopolno odražajo profil uporabnika. Pritisnite, da odprete polni profil v brskalniku.</string> + <string name="unpin_action">Odpni</string> + <string name="pin_action">Pripni</string> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> Spodbuda</item> + <item quantity="two"><b>%1$s</b> Spodbudi</item> + <item quantity="few"><b>%1$s</b> Spodbude</item> + <item quantity="other"><b>%1$s</b> Spodbud</item> + </plurals> + <string name="title_reblogged_by">Spodbudil/a</string> + <string name="title_favourited_by">Vzljubil/a</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s in %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s in %3$d več</string> + <string name="description_post_media">Mediji: %1$s</string> + <string name="description_post_cw">Opozorila o vsebini: %1$s</string> + <string name="description_post_media_no_description_placeholder">Brez opisa</string> + <string name="description_post_reblogged">Ponovno objavljen</string> + <string name="description_post_favourited">Priljubljene</string> + <string name="description_visibility_public">Javno</string> + <string name="description_visibility_unlisted">Ni prikazano</string> + <string name="description_visibility_private">Sledilci</string> + <string name="description_visibility_direct">Neposredno</string> + <string name="hint_list_name">Ime seznama</string> + <string name="edit_hashtag_hint">Ključnik brez #</string> + <string name="notifications_clear">Počisti</string> + <string name="notifications_apply_filter">Filter</string> + <string name="filter_apply">Uporabi</string> + <string name="compose_shortcut_long_label">Sestavi tut</string> + <string name="compose_shortcut_short_label">Sestavi</string> + <string name="pref_title_bot_overlay">Prikaži kazalnik za robote</string> + <string name="notification_clear_text">Ali ste prepričani, da želite trajno izbrisati vsa obvestila\?</string> + <string name="action_delete_and_redraft">Izbriši in preoblikuj</string> + <string name="dialog_redraft_post_warning">Izbriši in preoblikuj tut\?</string> + <string name="poll_info_format"> <!-- 15 votes • 1 hour left --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s glas</item> + <item quantity="two">%1$s glasova</item> + <item quantity="few">%1$s glasovi</item> + <item quantity="other">%1$s glasov</item> + </plurals> + <string name="poll_info_time_absolute">se konča ob %1$s</string> + <string name="poll_info_closed">zaprto</string> + <string name="poll_vote">Glasovanje</string> + <string name="pref_title_notification_filter_poll">ankete so se končala</string> + <string name="app_them_dark">Temna</string> + <string name="app_theme_light">Svetla</string> + <string name="app_theme_black">Črna</string> + <string name="app_theme_auto">Samodejno ob sončnem zahodu</string> + <string name="app_theme_system">Uporabi sistemsko temo</string> + <string name="post_privacy_public">Javno</string> + <string name="post_privacy_unlisted">Ni prikazano</string> + <string name="post_privacy_followers_only">Samo za sledilce</string> + <string name="post_text_size_smallest">Najmanjša</string> + <string name="post_text_size_small">Majhna</string> + <string name="post_text_size_medium">Srednja</string> + <string name="post_text_size_large">Velika</string> + <string name="post_text_size_largest">Največja</string> + <string name="notification_poll_name">Ankete</string> + <string name="notification_poll_description">Obvestilo o anketah, ki so se končale</string> + <string name="compose_preview_image_description">Dejanje za sliko %1$s</string> + <string name="poll_ended_voted">Anketa, na kateri ste glasovali, se je končala</string> + <string name="poll_ended_created">Anketa, ki ste jo ustvarili, se je končala</string> + <plurals name="poll_timespan_days"> + <item quantity="one">še %1$d dan</item> + <item quantity="two">še %1$d dni</item> + <item quantity="few">še %1$d dni</item> + <item quantity="other">še %1$d dni</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">še %1$d ura</item> + <item quantity="two">še %1$d uri</item> + <item quantity="few">še %1$d ure</item> + <item quantity="other">še %1$d ur</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">še %1$d minuta</item> + <item quantity="two">še %1$d minuti</item> + <item quantity="few">še %1$d minute</item> + <item quantity="other">še %1$d minut</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d sekunda</item> + <item quantity="two">%1$d sekundi</item> + <item quantity="few">%1$d sekunde</item> + <item quantity="other">%1$d sekund</item> + </plurals> + <string name="pref_title_animate_gif_avatars">Animirane podobe GIF</string> + <string name="description_poll">Anketa z izbiro: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="caption_notoemoji">Googlovi trenutni emotikoni</string> + <string name="button_continue">Nadaljuj</string> + <string name="button_back">Nazaj</string> + <string name="button_done">Končano</string> + <string name="report_sent_success">\@%1$s je uspešno prijavljen</string> + <string name="hint_additional_info">Dodatni komentarji</string> + <string name="report_remote_instance">Posreduj %1$s</string> + <string name="failed_report">Prijava je bila neuspešna</string> + <string name="failed_fetch_posts">Statusov ni bilo mogoče pridobiti</string> + <string name="report_description_1">Poročilo bo poslano moderatorju strežnika. Spodaj lahko navedete, zakaj prijavljate ta račun:</string> + <string name="report_description_remote_instance">Račun je iz drugega strežnika. Pošljem anonimno kopijo poročila tudi na drugi strežnik\?</string> + <string name="title_domain_mutes">Skrite domene</string> + <string name="action_view_domain_mutes">Skrite domene</string> + <string name="action_mute_domain">Utišaj %1$s</string> + <string name="confirmation_domain_unmuted">Domena %1$s odrita</string> + <string name="mute_domain_warning">Ali ste prepričani, da želite blokirati vse iz domene %1$s\? Vsebine iz te domene ne boste videli v nobeni javni časovnici ali v obvestilih. Vaši sledilci iz te domene bodo odstranjeni.</string> + <string name="mute_domain_warning_dialog_ok">Skrij celotno domeno</string> + <string name="filter_dialog_whole_word">Cela beseda</string> + <string name="filter_dialog_whole_word_description">Če je ključna beseda ali fraza samo alfanumerična, bo uporabljena le, če se ujema s celotno besedo</string> + <string name="title_accounts">Računi</string> + <string name="failed_search">Iskanje je bilo neuspešno</string> + <string name="pref_title_alway_open_spoiler">Vedno razširite tute, označene z opozorilom o vsebini</string> + <string name="action_add_poll">Dodaj anketo</string> + <string name="create_poll_title">Anketa</string> + <string name="duration_5_min">5 minut</string> + <string name="duration_30_min">30 minut</string> + <string name="duration_1_hour">1 ura</string> + <string name="duration_6_hours">6 ur</string> + <string name="duration_1_day">1 dan</string> + <string name="duration_3_days">3 dni</string> + <string name="duration_7_days">7 dni</string> + <string name="add_poll_choice">Dodaj izbiro</string> + <string name="poll_allow_multiple_choices">Več izbir</string> + <string name="poll_new_choice_hint">Izbira %1$d</string> + <string name="edit_poll">Uredi</string> + <string name="title_scheduled_posts">Napovedani tuti</string> + <string name="action_edit">Uredi</string> + <string name="action_access_scheduled_posts">Napovedani tuti</string> + <string name="action_reset_schedule">Ponastavi</string> + <string name="action_schedule_post">Napovej tut</string> + <string name="post_lookup_error_format">Napaka pri iskanju objave %1$s</string> + <string name="about_powered_by_tusky">Poganja ga Tusky</string> + <string name="post_boosted_format">%1$s spodbudil</string> + <string name="hashtags">Ključniki</string> + <string name="notification_follow_request_name">Zahteve za Sledenje</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Priljubljena</item> + <item quantity="two"><b>%1$s</b> Priljubljeni</item> + <item quantity="few"><b>%1$s</b> Priljubljene</item> + <item quantity="other"><b>%1$s</b> Priljubljenih</item> + </plurals> + <string name="error_multimedia_size_limit">Video in zvočne datoteke ne smejo presegati velikosti %1$s MB.</string> + <string name="error_muting_hashtag_format">Težava pri blokiranju #%1$s</string> + <string name="error_unmuting_hashtag_format">Neuspešno odkrivanje #%1$s</string> + <string name="error_following_hashtag_format">Težava pri sledenju #%1$s</string> + <string name="error_media_upload_sending_fmt">Nalaganje ni bilo uspešno: %1$s</string> + <string name="error_following_hashtags_unsupported">Ta instanca ne podpira možnosti ključnikov.</string> + <string name="error_blocking_domain">Neuspešno blokiranje %1$s: %2$s</string> + <string name="error_loading_account_details">Nalaganje podatkov o računu ni bilo uspešno</string> + <string name="error_could_not_load_login_page">Prijavne strani ni bilo mogoče naložiti.</string> + <string name="error_image_edit_failed">Datoteke ni bilo možno urediti.</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-small/integer.xml b/app/src/main/res/values-small/integer.xml new file mode 100644 index 0000000..9bcc55c --- /dev/null +++ b/app/src/main/res/values-small/integer.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <integer name="profile_media_column_count">2</integer> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml new file mode 100644 index 0000000..02960c5 --- /dev/null +++ b/app/src/main/res/values-sv/strings.xml @@ -0,0 +1,720 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Ett fel har uppstått.</string> + <string name="error_network">Ett nätverksfel uppstod. Kontrollera din anslutning och försök igen.</string> + <string name="error_empty">Detta kan inte vara tomt.</string> + <string name="error_invalid_domain">Ogiltig domän angiven</string> + <string name="error_failed_app_registration">Misslyckades att autentisera med den instansen. Om detta kvarstår, försök Logga in i webbläsaren från menyn.</string> + <string name="error_no_web_browser_found">Det gick inte att hitta en webbläsare.</string> + <string name="error_authorization_unknown">Ett oidentifierat auktoriseringsfel inträffade. Om detta kvarstår, försök Logga in i webbläsaren från menyn.</string> + <string name="error_authorization_denied">Auktorisering nekades. Om du är säker på att du har angett rätt referenser, prova Logga in i webbläsaren från menyn.</string> + <string name="error_retrieving_oauth_token">Misslyckades med att hämta en inloggningstoken. Om detta kvarstår, försök Logga in i webbläsaren från menyn.</string> + <string name="error_compose_character_limit">Inlägget är för långt!</string> + <string name="error_media_upload_type">Den typen av fil kan inte laddas upp.</string> + <string name="error_media_upload_opening">Den filen kunde inte öppnas.</string> + <string name="error_media_upload_permission">Behörighet att läsa media krävs.</string> + <string name="error_media_download_permission">Behörighet att spara media krävs.</string> + <string name="error_media_upload_image_or_video">Bilder och videoklipp kan inte båda bifogas i samma inlägg.</string> + <string name="error_media_upload_sending">Uppladdningen misslyckades.</string> + <string name="error_sender_account_gone">Kunde inte posta inlägg.</string> + <string name="title_home">Hem</string> + <string name="title_notifications">Aviseringar</string> + <string name="title_public_local">Lokalt</string> + <string name="title_public_federated">Federerat</string> + <string name="title_direct_messages">Direktmeddelanden</string> + <string name="title_tab_preferences">Flikar</string> + <string name="title_view_thread">Tråd</string> + <string name="title_posts">Inlägg</string> + <string name="title_posts_with_replies">Med svar</string> + <string name="title_posts_pinned">Fastnålade</string> + <string name="title_follows">Följer</string> + <string name="title_followers">Följare</string> + <string name="title_favourites">Favoriter</string> + <string name="title_mutes">Tystade användare</string> + <string name="title_blocks">Blockerade användare</string> + <string name="title_follow_requests">Följarförfrågningar</string> + <string name="title_edit_profile">Redigera din profil</string> + <string name="title_drafts">Utkast</string> + <string name="title_licenses">Licenser</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s knuffade</string> + <string name="post_sensitive_media_title">Känsligt innehåll</string> + <string name="post_media_hidden_title">Dold media</string> + <string name="post_sensitive_media_directions">Tryck för att visa</string> + <string name="post_content_warning_show_more">Visa mer</string> + <string name="post_content_warning_show_less">Visa mindre</string> + <string name="post_content_show_more">Expandera</string> + <string name="post_content_show_less">Dölj</string> + <string name="message_empty">Ingenting här.</string> + <string name="footer_empty">Inget här. Dra ner för att uppdatera!</string> + <string name="notification_reblog_format">%1$s puffade ditt inlägg</string> + <string name="notification_favourite_format">%1$s favoritmarkerade ditt inlägg</string> + <string name="notification_follow_format">%1$s följer dig</string> + <string name="report_username_format">Rapportera @%1$s</string> + <string name="report_comment_hint">Ytterligare kommentarer?</string> + <string name="action_quick_reply">Snabbsvar</string> + <string name="action_reply">Svara</string> + <string name="action_reblog">Knuffa</string> + <string name="action_unreblog">Ta bort knuff</string> + <string name="action_favourite">Favorit</string> + <string name="action_unfavourite">Ta bort favorit</string> + <string name="action_more">Mer</string> + <string name="action_compose">Skriv</string> + <string name="action_login">Logga in med Tusky</string> + <string name="action_logout">Logga ut</string> + <string name="action_logout_confirm">Är du säker på att du vill logga ut från %1$s\? Alla lokala data om kontot kommer att raderas, inklusive utkast och inställningar.</string> + <string name="action_follow">Följ</string> + <string name="action_unfollow">Avfölj</string> + <string name="action_block">Blockera</string> + <string name="action_unblock">Avblockera</string> + <string name="action_hide_reblogs">Dölj knuffar</string> + <string name="action_show_reblogs">Visa knuffar</string> + <string name="action_report">Rapportera</string> + <string name="action_delete">Radera</string> + <string name="action_send">TOOT</string> + <string name="action_send_public">TOOT!</string> + <string name="action_retry">Försök igen</string> + <string name="action_close">Stäng</string> + <string name="action_view_profile">Profil</string> + <string name="action_view_preferences">Inställningar</string> + <string name="action_view_account_preferences">Kontospecifika inställningar</string> + <string name="action_view_favourites">Favoriter</string> + <string name="action_view_mutes">Tystade användare</string> + <string name="action_view_blocks">Blockerade användare</string> + <string name="action_view_follow_requests">Följarförfrågningar</string> + <string name="action_view_media">Media</string> + <string name="action_open_in_web">Öppna i webbläsare</string> + <string name="action_add_media">Lägg till media</string> + <string name="action_photo_take">Ta foto</string> + <string name="action_share">Dela</string> + <string name="action_mute">Tysta</string> + <string name="action_unmute">Ta bort tystade användare</string> + <string name="action_mention">Omnämn</string> + <string name="action_hide_media">Dölj media</string> + <string name="action_open_drawer">Öppna låda</string> + <string name="action_save">Spara</string> + <string name="action_edit_profile">Redigera profil</string> + <string name="action_edit_own_profile">Redigera</string> + <string name="action_undo">Ångra</string> + <string name="action_accept">Acceptera</string> + <string name="action_reject">Avvisa</string> + <string name="action_search">Sök</string> + <string name="action_access_drafts">Utkast</string> + <string name="action_toggle_visibility">Inläggssynlighet</string> + <string name="action_content_warning">Innehållsvarning</string> + <string name="action_emoji_keyboard">Emoji-tangentbord</string> + <string name="action_add_tab">Lägg till flik</string> + <string name="action_links">Länkar</string> + <string name="action_mentions">Omnämnanden</string> + <string name="action_hashtags">Hashtaggar</string> + <string name="action_open_reblogger">Öppna knuff författare</string> + <string name="action_open_reblogged_by">Visa knuffar</string> + <string name="action_open_faved_by">Visa favoriter</string> + <string name="title_hashtags_dialog">Hashtaggar</string> + <string name="title_mentions_dialog">Omnämnanden</string> + <string name="title_links_dialog">Länkar</string> + <string name="action_open_media_n">Öppna media #%1$d</string> + <string name="download_image">Laddar ned %1$s</string> + <string name="action_copy_link">Kopiera länk</string> + <string name="action_open_as">Öppna med %1$s</string> + <string name="action_share_as">Dela som …</string> + <string name="send_post_link_to">Dela toot-URL till…</string> + <string name="send_post_content_to">Dela toot till…</string> + <string name="send_media_to">Dela media till…</string> + <string name="confirmation_reported">Skickat!</string> + <string name="confirmation_unblocked">Användare avblockerad</string> + <string name="confirmation_unmuted">Användaren är inte tystad längre</string> + <string name="hint_domain">Vilken instans?</string> + <string name="hint_compose">Vad händer?</string> + <string name="hint_content_warning">Innehållsvarning</string> + <string name="hint_display_name">Visningsnamn</string> + <string name="hint_note">Biografi</string> + <string name="hint_search">Sök…</string> + <string name="search_no_results">Inga resultat</string> + <string name="label_quick_reply">Svara…</string> + <string name="label_avatar">Profilbild</string> + <string name="label_header">Bakgrundsbild</string> + <string name="link_whats_an_instance">Vad är en instans?</string> + <string name="login_connection">Ansluter…</string> + <string name="dialog_whats_an_instance">Adressen eller domänen för varje instans kan anges + här, till exempel mastodon.social, icosahedron.website, social.tchncs.de och + <a href="https://instances.social"> mer! </a> + \n\nOm du inte har något konto kan du ange namnet på instansen du vill ansluta till och skapa ett konto där. + \n\nEn instans är en plats där ditt konto finns, men du kan enkelt kommunicera med och följa andra personer på andra instanser, + som om du var på samma sajt. + \n\nMer information finns på <a href="https://joinmastodon.org">joinmastodon.org</a>. + </string> + <string name="dialog_title_finishing_media_upload">Uppladdning av media</string> + <string name="dialog_message_uploading_media">Laddar upp…</string> + <string name="dialog_download_image">Ladda ned</string> + <string name="dialog_message_cancel_follow_request">Återkalla följningsförfrågan?</string> + <string name="dialog_unfollow_warning">Sluta följ detta konto\?</string> + <string name="dialog_delete_post_warning">Radera denna toot?</string> + <string name="visibility_public">Offentlig: Skicka till offentliga tidslinjer</string> + <string name="visibility_unlisted">Olistad: Visa inte i offentliga tidslinjer</string> + <string name="visibility_private">Enbart-följare: Ses enbart av följare</string> + <string name="visibility_direct">Direkt: Skicka endast till nämnda användare</string> + <string name="pref_title_edit_notification_settings">Aviseringar</string> + <string name="pref_title_notifications_enabled">Aviseringar</string> + <string name="pref_title_notification_alerts">Alarm</string> + <string name="pref_title_notification_alert_sound">Meddela med ljud</string> + <string name="pref_title_notification_alert_vibrate">Meddela med vibration</string> + <string name="pref_title_notification_alert_light">Notifieringar med LED</string> + <string name="pref_title_notification_filters">Meddela mig när</string> + <string name="pref_title_notification_filter_mentions">omnämnd</string> + <string name="pref_title_notification_filter_follows">nya följare</string> + <string name="pref_title_notification_filter_reblogs">mina inlägg är knuffade</string> + <string name="pref_title_notification_filter_favourites">mina inlägg är favoritmarkerade</string> + <string name="pref_title_appearance_settings">Utseende</string> + <string name="pref_title_app_theme">Apptema</string> + <string name="pref_title_timelines">Tidslinjer</string> + <string name="pref_title_timeline_filters">Filter</string> + <string name="app_them_dark">Mörkt</string> + <string name="app_theme_light">Ljust</string> + <string name="app_theme_black">Svart</string> + <string name="app_theme_auto">Automatiskt vid solnedgång</string> + <string name="app_theme_system">Använd system-tema</string> + <string name="pref_title_browser_settings">Webbläsare</string> + <string name="pref_title_custom_tabs">Använd Chrome-anpassade flikar</string> + <string name="pref_title_language">Språk</string> + <string name="pref_title_post_filter">Filtrering av tidslinje</string> + <string name="pref_title_post_tabs">Hem tidslinje</string> + <string name="pref_title_show_boosts">Visa knuffar</string> + <string name="pref_title_show_replies">Visa svar</string> + <string name="pref_title_show_media_preview">Visa en förhandsgranskning</string> + <string name="pref_title_proxy_settings">Proxyserver</string> + <string name="pref_title_http_proxy_settings">HTTP-proxy</string> + <string name="pref_title_http_proxy_enable">Aktivera HTTP-proxy</string> + <string name="pref_title_http_proxy_server">HTTP-proxyserver</string> + <string name="pref_title_http_proxy_port">HTTP-proxyport</string> + <string name="pref_default_post_privacy">Standardinställning för inlägg</string> + <string name="pref_default_media_sensitivity">Markera alltid media som känsligt</string> + <string name="pref_publishing">Publicering (synkroniserad med server)</string> + <string name="pref_failed_to_sync">Misslyckades med att synkronisera inställningarna</string> + <string name="post_privacy_public">Offentlig</string> + <string name="post_privacy_unlisted">Olistad</string> + <string name="post_privacy_followers_only">Endast följare</string> + <string name="pref_post_text_size">Textstorlek på status</string> + <string name="post_text_size_smallest">Minsta</string> + <string name="post_text_size_small">Liten</string> + <string name="post_text_size_medium">Mellan</string> + <string name="post_text_size_large">Stor</string> + <string name="post_text_size_largest">Största</string> + <string name="notification_mention_name">Nya omnämningar</string> + <string name="notification_mention_descriptions">Aviseringar om nya omnämnanden</string> + <string name="notification_follow_name">Nya följare</string> + <string name="notification_follow_description">Aviseringar på nya följare</string> + <string name="notification_boost_name">Knuffar</string> + <string name="notification_boost_description">Aviseringar när dina inlägg blir puffade</string> + <string name="notification_favourite_name">Favoriter</string> + <string name="notification_favourite_description">Aviseringar när dina inlägg blir favoritmarkerade</string> + <string name="notification_mention_format">%1$s omnämnde dig</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s och %4$d andra</string> + <string name="notification_summary_medium">%1$s, %2$s, och %3$s</string> + <string name="notification_summary_small">%1$s och %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d ny interaktion</item> + <item quantity="other">%1$d nya interaktioner</item> + </plurals> + <string name="description_account_locked">Låst konto</string> + <string name="about_title_activity">Om</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tusky är fri programvara med öppen källkod. Det är licensierat under GNU General Public License version 3. Du kan läsa mer om licensen här: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">Tuskys webbsida: https://tusky.app</string> + <string name="about_bug_feature_request_site">Buggrapporter & funktionsförslag: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Tuskys Profil</string> + <string name="post_share_content">Dela innehåll av toot</string> + <string name="post_share_link">Dela länk till toot</string> + <string name="post_media_images">Bilder</string> + <string name="post_media_video">Video</string> + <string name="state_follow_requested">Följarförfrågan</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">om %1$dy</string> + <string name="abbreviated_in_days">om %1$dd</string> + <string name="abbreviated_in_hours">om %1$dh</string> + <string name="abbreviated_in_minutes">om %1$dm</string> + <string name="abbreviated_in_seconds">om %1$ds</string> + <string name="abbreviated_years_ago">%1$dy</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="follows_you">Följer dig</string> + <string name="pref_title_alway_show_sensitive_media">Visa alltid allt innehåll (inkl. känsligt)</string> + <string name="title_media">Media</string> + <string name="replying_to">Svarar till @%1$s</string> + <string name="load_more_placeholder_text">ladda mer</string> + <string name="pref_title_public_filter_keywords">Offentliga tidslinjer</string> + <string name="pref_title_thread_filter_keywords">Konversationer</string> + <string name="filter_addition_title">Lägg till filter</string> + <string name="filter_edit_title">Redigera filter</string> + <string name="filter_dialog_remove_button">Ta bort</string> + <string name="filter_dialog_update_button">Uppdatera</string> + <string name="filter_add_description">Filtrera fras</string> + <string name="add_account_name">Lägg till konto</string> + <string name="add_account_description">Lägg till ett nytt Mastodon-konto</string> + <string name="action_lists">Listor</string> + <string name="title_lists">Listor</string> + <string name="error_create_list">Kunde inte skapa lista</string> + <string name="error_rename_list">Kunde inte uppdatera listan</string> + <string name="error_delete_list">Kunde inte radera lista</string> + <string name="action_create_list">Skapa en lista</string> + <string name="action_rename_list">Uppdatera listan</string> + <string name="action_delete_list">Ta bort listan</string> + <string name="hint_search_people_list">Sök efter personer du följer</string> + <string name="action_add_to_list">Lägg till konto i listan</string> + <string name="action_remove_from_list">Ta bort kontot från listan</string> + <string name="compose_active_account_description">Publicerar som %1$s</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">Beskriv innehåll för synskadade (max %1$d tecken)</item> + <item quantity="other">Beskriv innehåll för synskadade (max %1$d tecken)</item> + </plurals> + <string name="action_set_caption">Ange bildtext</string> + <string name="action_remove">Ta bort</string> + <string name="lock_account_label">Lås konto</string> + <string name="lock_account_label_description">Kräver att du manuellt godkänner följare</string> + <string name="compose_save_draft">Spara utkast?</string> + <string name="send_post_notification_title">Skickar toot…</string> + <string name="send_post_notification_error_title">Kunde inte skicka toot</string> + <string name="send_post_notification_channel_name">Skickar toot</string> + <string name="send_post_notification_cancel_title">Sändning avbruten</string> + <string name="send_post_notification_saved_content">En kopia av tooten har sparats i dina utkast</string> + <string name="action_compose_shortcut">Skriv</string> + <string name="error_no_custom_emojis">Din instans %1$s har inga anpassade emojis</string> + <string name="emoji_style">Emojis</string> + <string name="system_default">Systemstandard</string> + <string name="download_fonts">Du behöver ladda ned dessa emojis först</string> + <string name="performing_lookup_title">Utför sökning…</string> + <string name="expand_collapse_all_posts">Expandera/Dölj alla statusar</string> + <string name="action_open_post">Öppna toot</string> + <string name="restart_required">Omstart av appen krävs</string> + <string name="restart_emoji">Du måste starta om Tusky för att tillämpa ändringarna</string> + <string name="later">Senare</string> + <string name="restart">Starta om</string> + <string name="caption_systememoji">Standard-emojis för din enhet</string> + <string name="caption_blobmoji">Emojis baserade på The Blob emojis från Android 4.4–7.1</string> + <string name="caption_twemoji">Mastodon\'s standard emojis</string> + <string name="download_failed">Nedladdning misslyckad</string> + <string name="profile_badge_bot_text">Robot</string> + <string name="account_moved_description">%1$s har flyttat till:</string> + <string name="reblog_private">Knuffa till ursprunglig målgrupp</string> + <string name="unreblog_private">Ta bort knuff</string> + <string name="license_description">Tusky innehåller kod och tillgångar från följande öppen källkodsprojekt:</string> + <string name="license_apache_2">Licensierad under the Apache License (kopia nedanför)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Profil-metadata</string> + <string name="profile_metadata_add">Spara ändringar</string> + <string name="profile_metadata_label_label">Etikett</string> + <string name="profile_metadata_content_label">Innehåll</string> + <string name="pref_title_absolute_time">Använd absolut tid</string> + <string name="label_remote_account">Information nedan kan visa en inkomplett profil. Tryck för att öppna profilen i webbläsaren.</string> + <string name="unpin_action">Ta bort nål</string> + <string name="pin_action">Nåla fast</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Favorit</item> + <item quantity="other"><b>%1$s</b> Favoriter</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> Knuff</item> + <item quantity="other"><b>%1$s</b> Knuffar</item> + </plurals> + <string name="title_reblogged_by">Knuffad av</string> + <string name="title_favourited_by">Favoritmarkerad av</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s och %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s och %3$d mer</string> + <string name="description_post_media">Media: %1$s</string> + <string name="description_post_cw">Innehållsvarning: %1$s</string> + <string name="description_post_media_no_description_placeholder"> Ingen beskrivning + </string> + <string name="description_post_reblogged"> Knuffad + </string> + <string name="description_post_favourited">Favoritmarkerat</string> + <string name="description_visibility_public"> Publik + </string> + <string name="description_visibility_unlisted"> Olistad + </string> + <string name="description_visibility_private"> Följare + </string> + <string name="description_visibility_direct"> Direkt + </string> + <string name="hint_list_name">Listnamn</string> + <string name="download_media">Ladda ned media</string> + <string name="downloading_media">Laddar ned media</string> + <string name="edit_hashtag_hint">Hashtag utan #</string> + <string name="compose_shortcut_long_label">Skriv inlägg</string> + <string name="compose_shortcut_short_label">Skriv</string> + <string name="notifications_clear">Radera</string> + <string name="notifications_apply_filter">Filtrera</string> + <string name="filter_apply">Utför</string> + <string name="pref_title_bot_overlay">Visa robotindikator</string> + <string name="notification_clear_text">Är du säker på att du vill rensa dina aviseringar permanent\?</string> + <string name="action_delete_and_redraft">Radera och skriv på nytt</string> + <string name="dialog_redraft_post_warning">Radera och skriv ny toot\?</string> + <string name="poll_info_format"> <!-- 15 röster • 1 timme kvar --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s röst</item> + <item quantity="other">%1$s röster</item> + </plurals> + <string name="poll_info_time_absolute">avslutas vid %1$s</string> + <string name="poll_info_closed">stängd</string> + <string name="poll_vote">Rösta</string> + <string name="pref_title_notification_filter_poll">omröstningar har avslutats</string> + <string name="notification_poll_name">Omröstningar</string> + <string name="notification_poll_description">Aviseringar när omröstningar har avslutats</string> + <string name="poll_ended_voted">En omröstning där du har röstat är avslutad</string> + <string name="poll_ended_created">En omröstning som du har skapat har avslutats</string> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d dag kvar</item> + <item quantity="other">%1$d dagar kvar</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d timme kvar</item> + <item quantity="other">%1$d timmar kvar</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d minut kvar</item> + <item quantity="other">%1$d minuter kvar</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d sekund kvar</item> + <item quantity="other">%1$d sekunder kvar</item> + </plurals> + <string name="compose_preview_image_description">Åtgärder för bild %1$s</string> + <string name="pref_title_animate_gif_avatars">Animera profil gifar</string> + <string name="description_poll">Omröstning med valen: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="title_domain_mutes">Dolda domäner</string> + <string name="action_view_domain_mutes">Dolda domäner</string> + <string name="action_mute_domain">Tysta %1$s</string> + <string name="confirmation_domain_unmuted">%1$s inte tystad längre</string> + <string name="mute_domain_warning">Är du säker på att du vill blockera allt från %1$s\? Du kommer inte kunna se något innehåll från denna domän i publika tidslinjer eller i dina notifieringar. Dina följare på domänen kommer inte att bli borttagna.</string> + <string name="mute_domain_warning_dialog_ok">Dölj hela domänen</string> + <string name="caption_notoemoji">Google\'s nuvarande emojis</string> + <string name="button_continue">Fortsätt</string> + <string name="button_back">Tillbaka</string> + <string name="button_done">Klar</string> + <string name="report_sent_success">Anmälan av @%1$s har lyckats</string> + <string name="hint_additional_info">Ytterligare kommentarer</string> + <string name="report_remote_instance">Vidarebefordra till %1$s</string> + <string name="failed_report">Misslyckades att anmäla</string> + <string name="failed_fetch_posts">Misslyckades att hämta status</string> + <string name="report_description_1">Anmälan kommer att skickas till din servermoderator. Du kan beskriva varför du anmäler kontot nedan:</string> + <string name="report_description_remote_instance">Kontot är från en annan server. Skicka en anonym kopia av anmälan dit också\?</string> + <string name="filter_dialog_whole_word">Helt ord</string> + <string name="filter_dialog_whole_word_description">När nyckelordet eller frasen enbart är alfanumerisk, appliceras den om den matchar hela ordet</string> + <string name="pref_title_alway_open_spoiler">Expandera alltid inlägg med innehållsvarningar</string> + <string name="title_accounts">Konton</string> + <string name="failed_search">Sökning misslyckades</string> + <string name="action_add_poll">Skapa en omröstning</string> + <string name="create_poll_title">Omröstning</string> + <string name="duration_5_min">5 minuter</string> + <string name="duration_30_min">30 minuter</string> + <string name="duration_1_hour">1 timme</string> + <string name="duration_6_hours">6 timmar</string> + <string name="duration_1_day">1 dag</string> + <string name="duration_3_days">3 dagar</string> + <string name="duration_7_days">7 dagar</string> + <string name="add_poll_choice">Lägg till alternativ</string> + <string name="poll_allow_multiple_choices">Flerval</string> + <string name="poll_new_choice_hint">Val %1$d</string> + <string name="edit_poll">Redigera</string> + <string name="title_scheduled_posts">Schemalagda toots</string> + <string name="action_edit">Redigera</string> + <string name="action_access_scheduled_posts">Schemalagda toots</string> + <string name="action_schedule_post">Schemalägg toot</string> + <string name="action_reset_schedule">Återställ</string> + <string name="post_lookup_error_format">Fel vid uppslagning av status %1$s</string> + <string name="title_bookmarks">Bokmärken</string> + <string name="action_bookmark">Bokmärk</string> + <string name="action_view_bookmarks">Bokmärken</string> + <string name="about_powered_by_tusky">Drivs av Tusky</string> + <string name="description_post_bookmarked">Bokmärkt</string> + <string name="select_list_title">Välj lista</string> + <string name="list">Lista</string> + <string name="no_scheduled_posts">Du har inga schemalagda statusar.</string> + <string name="no_drafts">Du har inga utkast.</string> + <string name="warning_scheduling_interval">Mastodon har ett minimalt schemaläggningsintervall på 5 minuter.</string> + <string name="action_mute_conversation">Tysta konversation</string> + <string name="pref_title_confirm_reblogs">Visa bekräftelse innan knuff</string> + <string name="pref_title_show_cards_in_timelines">Visa förhandsvisning av länkar i tidslinjen</string> + <string name="pref_title_enable_swipe_for_tabs">Aktivera sveprörelser för att växla mellan flikar</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s person</item> + <item quantity="other">%1$s personer</item> + </plurals> + <string name="notification_follow_request_description">Aviseringar om följarförfrågningar</string> + <string name="notification_follow_request_name">Följförfrågningar</string> + <string name="pref_title_notification_filter_follow_requests">följförfrågan</string> + <string name="dialog_mute_warning">Tysta @%1$s\?</string> + <string name="dialog_block_warning">Blockera @%1$s\?</string> + <string name="action_unmute_conversation">Aktivera ljud på konversation</string> + <string name="notification_follow_request_format">%1$s vill följa dig</string> + <string name="add_hashtag_title">Lägg till hashtag</string> + <string name="hashtags">Hashtaggar</string> + <string name="pref_main_nav_position_option_top">Topp</string> + <string name="pref_main_nav_position_option_bottom">Botten</string> + <string name="pref_main_nav_position">Standard navigationsposition</string> + <string name="pref_title_gradient_for_media">Visa färgglada gradienter för gömd media</string> + <string name="action_unmute_domain">Ta bort tystad %1$s</string> + <string name="action_unmute_desc">Ta bort tystad %1$s</string> + <string name="pref_title_hide_top_toolbar">Dölj titeln i övre verktygsfältet</string> + <string name="dialog_mute_hide_notifications">Dölj aviseringar</string> + <string name="account_note_saved">Sparat!</string> + <string name="account_note_hint">Din privata notering om detta kontot</string> + <string name="no_announcements">Det finns inga meddelanden.</string> + <string name="title_announcements">Meddelanden</string> + <string name="notification_subscription_description">Aviseringar när någon du följer skrivit ett nytt inlägg</string> + <string name="notification_subscription_name">Nya inlägg</string> + <string name="pref_title_notification_filter_subscriptions">någon jag följer har skrivit ett nytt inlägg</string> + <string name="notification_subscription_format">%1$s skrev precis</string> + <string name="wellbeing_hide_stats_profile">Dölj kvantitativ information på profiler</string> + <string name="wellbeing_hide_stats_posts">Dölj kvantitativ information på inlägg</string> + <string name="limit_notifications">Begränsa tidslinje aviseringar</string> + <string name="review_notifications">Revidera aviseringar</string> + <string name="wellbeing_mode_notice">Information som kan påverka ditt välmående kommer att döljas. Detta inkluderar: +\n +\n- Favoritmarkering-/Knuff-/Följaraviseringar +\n- Favoritmarkering/Antal knuffar på inlägg +\n- Följare/Inläggsstatistik på profiler +\n +\nPush-aviseringar påverkas inte, men du ändra dina aviseringinställningar manuellt.</string> + <string name="pref_title_wellbeing_mode">Välmående</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">Du kan inte ladda upp fler än %1$d mediebilaga.</item> + <item quantity="other">Du kan inte ladda upp fler än %1$d mediebilagor.</item> + </plurals> + <string name="delete_scheduled_post_warning">Radera detta schemalagda inlägg\?</string> + <string name="instance_rule_info">Genom att logga in accepterar du reglerna på %1$s.</string> + <string name="instance_rule_title">%1$s regler</string> + <string name="error_unfollowing_hashtag_format">Kunde inte avfölja #%1$s</string> + <string name="dialog_delete_conversation_warning">Radera denna konversation\?</string> + <string name="pref_show_self_username_always">Alltid</string> + <string name="dialog_push_notification_migration">För att använda pushnotiser via UnifiedPush behöver Tusky din tillåtelse att prenumerera på notiser på din Mastodon-server. Detta kräver att du loggar in igen för att ändra vilka OAuth-scopes Tusky har tillgång till. Genom använda alternativet logga in igen här eller i Kontoinställningarna behåller du alla dina lokala utkast och data i cache.</string> + <string name="set_focus_description">Tryck eller dra cirkeln för att välja fokuspunkten som alltid kommer synas i miniatyrbilder.</string> + <string name="label_duration">Varaktighet</string> + <string name="duration_indefinite">Oändligt</string> + <string name="dialog_push_notification_migration_other_accounts">Du har loggat in igen på ditt konto för att ge Tusky tillgång till push-prenumeration. Dock har du andra konton som inte har migrerats såhär ännu. Växla till dem och logga in igen för att aktivera stöd för UnifiedPush-notiser.</string> + <string name="action_unsubscribe_account">Sluta prenumerera</string> + <string name="description_post_language">Inläggsspråk</string> + <string name="pref_show_self_username_disambiguate">När flera konton är inloggade</string> + <string name="pref_show_self_username_never">Aldrig</string> + <string name="notification_sign_up_name">Registreringar</string> + <string name="notification_sign_up_description">Notiser om nya användare</string> + <string name="notification_update_name">Inläggsredigeringar</string> + <string name="notification_update_description">Notiser när inlägg du interagerat med redigerats</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="action_edit_image">Redigera bild</string> + <string name="duration_14_days">14 dagar</string> + <string name="duration_30_days">30 dagar</string> + <string name="duration_60_days">60 dagar</string> + <string name="duration_90_days">90 dagar</string> + <string name="duration_no_change">(Ingen ändring)</string> + <string name="pref_title_show_self_username">Visa användarnamn i verktygsrader</string> + <string name="pref_title_confirm_favourites">Visa bekräftelse före favoritmarkering</string> + <string name="dialog_delete_list_warning">Vill du verkligen radera listan %1$s\?</string> + <string name="drafts_post_failed_to_send">Det här inlägget kunde inte skickas!</string> + <string name="drafts_failed_loading_reply">Kunde inte ladda information om svar</string> + <string name="draft_deleted">Utkast raderat</string> + <string name="drafts_post_reply_removed">Inlägget du skrev ett utkast till svar på har raderats</string> + <string name="follow_requests_info">Även om ditt konto inte är låst så tänker administratörerna på %1$s att du kanske ändå vill granska följförfrågan från dessa konton manuellt.</string> + <string name="action_subscribe_account">Prenumerera</string> + <string name="tusky_compose_post_quicksetting_label">Skriv inlägg</string> + <string name="account_date_joined">Gick med i %1$s</string> + <string name="saving_draft">Sparar utkast…</string> + <string name="tips_push_notification_migration">Logga in igen på alla konton för att tillåta pushnotiser.</string> + <string name="error_multimedia_size_limit">Video- och ljudfiler kan inte överskrida %1$s MB i storlek.</string> + <string name="error_image_edit_failed">Bilden kunde inte redigeras.</string> + <string name="duration_365_days">365 dagar</string> + <string name="duration_180_days">180 dagar</string> + <string name="action_set_focus">Sätt fokuspunkt</string> + <string name="error_following_hashtag_format">Kunde inte följa #%1$s</string> + <string name="notification_sign_up_format">%1$s registrerade sig</string> + <string name="notification_update_format">%1$s redigerade sitt inlägg</string> + <string name="action_unbookmark">Ta bort bokmärke</string> + <string name="action_delete_conversation">Radera konversation</string> + <string name="pref_title_notification_filter_sign_ups">någon registrerade sig</string> + <string name="pref_title_notification_filter_updates">ett inlägg jag interagerat med har redigerats</string> + <string name="pref_title_animate_custom_emojis">Animera skräddarsydda emojis</string> + <string name="post_media_audio">Ljud</string> + <string name="post_media_attachments">Bilagor</string> + <string name="status_count_one_plus">1+</string> + <string name="title_migration_relogin">Logga in igen för pushnotiser</string> + <string name="action_dismiss">Avvisa</string> + <string name="action_details">Detaljer</string> + <string name="error_loading_account_details">Kunde inte ladda kontodetaljer</string> + <string name="title_login">Logga in</string> + <string name="error_could_not_load_login_page">Kunde inte ladda inloggningssidan.</string> + <string name="compose_save_draft_loses_media">Spara utkast\? (Bilagor kommer att laddas upp igen när du återställer utkastet.)</string> + <string name="failed_to_pin">Kunde inte fästa</string> + <string name="failed_to_unpin">Kunde inte lossa</string> + <string name="action_add_reaction">lägg till reaktion</string> + <string name="report_category_violation">Regelbrott</string> + <string name="action_add_or_remove_from_list">Lägg till eller ta bort från lista</string> + <string name="failed_to_add_to_list">Kunde inte lägga till kontot till listan</string> + <string name="failed_to_remove_from_list">Kunde inte ta bort kontot från listan</string> + <string name="no_lists">Du har inga listor.</string> + <string name="action_unfollow_hashtag_format">Sluta följa #%1$s\?</string> + <string name="status_created_at_now">nu</string> + <string name="notification_report_format">Ny rapport för %1$s</string> + <string name="notification_header_report_format">%1$s rapporterade %2$s</string> + <string name="notification_summary_report_format">%1$s · %2$d inlägg bifogade</string> + <string name="confirmation_hashtag_unfollowed">#%1$s avföljd</string> + <string name="pref_title_notification_filter_reports">det finns en ny rapport</string> + <string name="notification_report_name">Rapporter</string> + <string name="notification_report_description">Aviseringar om modereringsrapporter</string> + <string name="report_category_spam">Skräppost</string> + <string name="report_category_other">Annat</string> + <string name="error_following_hashtags_unsupported">Den här servern har inte stöd för att följa hashtaggar.</string> + <string name="title_followed_hashtags">Följda hashtaggar</string> + <string name="pref_default_post_language">Standardspråk för inlägg</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="description_post_edited">Redigerade</string> + <string name="post_edited">Redigerade %1$s</string> + <string name="error_muting_hashtag_format">Kunde inte tysta #%1$s, fel uppstod</string> + <string name="error_unmuting_hashtag_format">Kunde inte avtysta #%1$s, fel uppstod</string> + <string name="hint_media_description_missing">Media bör ha en beskrivning.</string> + <string name="pref_title_http_proxy_port_message">Portnummer bör vara mellan %1$d och %2$d</string> + <string name="error_status_source_load">Kunde inte ladda status från servern.</string> + <string name="post_media_alt">ALT</string> + <string name="a11y_label_loading_thread">Laddar tråd</string> + <string name="mute_notifications_switch">Tysta notiser</string> + <string name="title_edits">Redigeringar</string> + <string name="status_edit_info">%1$s redigerade</string> + <string name="action_share_account_link">Dela länk med konto</string> + <string name="action_share_account_username">Dela användarnamn för konto</string> + <string name="send_account_link_to">Dela konto-URL med…</string> + <string name="send_account_username_to">Dela kontots användarnamn med…</string> + <string name="account_username_copied">Kopierade användarnamn</string> + <string name="status_created_info">%1$s skapade</string> + <string name="action_discard">Förkasta ändringar</string> + <string name="action_continue_edit">Fortsätt redigera</string> + <string name="compose_unsaved_changes">Du har ändringar som inte sparats.</string> + <string name="action_post_failed">Uppladdning misslyckades</string> + <string name="action_post_failed_detail">Ett fel inträffade när inlägget skulle laddas upp och har sparats till utkast. +\n +\nAntingen kunde servern inte kontaktas, eller så nekades uppladdningen.</string> + <string name="action_post_failed_show_drafts">Visa utkast</string> + <string name="action_browser_login">Inloggning via webbläsaren</string> + <string name="pref_summary_http_proxy_disabled">Inaktiverad</string> + <string name="pref_summary_http_proxy_missing"><ej angiven></string> + <string name="pref_summary_http_proxy_invalid"><felaktig></string> + <string name="action_post_failed_detail_plural">Uppladdning av dina inlägg misslyckades och de har sparats i utkast. +\n +\nAntingen kunde inte servern nås eller så har uppladdningen nekats.</string> + <string name="action_post_failed_do_nothing">Avbryt</string> + <string name="description_login">Fungerar i de flesta fallen. Ingen information läcker till andra applikationer.</string> + <string name="pref_reading_order_oldest_first">Äldsta först</string> + <string name="pref_reading_order_newest_first">Nyaste först</string> + <string name="dialog_follow_hashtag_title">Följ hashtagg</string> + <string name="dialog_follow_hashtag_hint">#hashtagg</string> + <string name="accessibility_talking_about_tag">%1$d personer pratar om hashtagg %2$s</string> + <string name="total_usage">Total användning</string> + <string name="total_accounts">Totala konton</string> + <string name="post_media_image">Bild</string> + <string name="description_browser_login">Kan stödja ytterligare autentiseringsmetoder, men kräver en webbläsare som stöds.</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="filter_keyword_addition_title">Lägg till nyckelord</string> + <string name="filter_edit_keyword_title">Redigera nyckelord</string> + <string name="notification_unknown_name">Okänd</string> + <string name="socket_timeout_exception">Att kontakta din server tog för lång tid</string> + <string name="ui_error_unknown">okänd anledning</string> + <string name="ui_success_accepted_follow_request">Följ begäran accepterad</string> + <string name="ui_success_rejected_follow_request">Följ begäran blockerad</string> + <string name="select_list_manage">Hantera listor</string> + <string name="status_filtered_show_anyway">Visa allafall</string> + <string name="status_filter_placeholder_label_format">Filtrerad: %1$s</string> + <string name="pref_title_account_filter_keywords">Profiler</string> + <string name="label_filter_action">Filteråtgärd</string> + <string name="label_filter_keywords">Nyckelord eller fraser att filtrera</string> + <string name="pref_title_show_stat_inline">Visa post statistik i tidslinje</string> + <string name="hint_filter_title">Mitt filter</string> + <string name="label_filter_title">Titel</string> + <string name="filter_action_warn">Varna</string> + <string name="filter_action_hide">Dölj</string> + <string name="filter_description_warn">Dölj med en varning</string> + <string name="filter_description_hide">Dölj helt</string> + <string name="action_add">Lägg till</string> + <string name="filter_keyword_display_format">%1$s (helt ord)</string> + <string name="help_empty_home">Detta är din <b>hemtidslinje</b>. Den visar de senaste inläggen för konton du följer. +\n +\nFör att utforska konton kan du antingen upptäcka dem i någon av de andra tidslinjerna. Till exempel den lokala tidslinjen för din instans [iconics gmd_group]. Eller så kan du söka efter dem efter namn [ikonics gmd_search]; sök till exempel efter Tusky för att hitta vårt Mastodon-konto.</string> + <string name="pref_ui_text_size">UI textstorlek</string> + <string name="notification_listenable_worker_name">Bakgrundsaktivitet</string> + <string name="notification_listenable_worker_description">Aviseringar när Tusky arbetar i bakgrunden</string> + <string name="notification_notification_worker">Hämtar aviseringar…</string> + <string name="notification_prune_cache">Cacheunderhåll…</string> + <string name="action_refresh">Uppdatera</string> + <string name="ui_error_reblog">Att knuffa inlägg misslyckades: %1$s</string> + <string name="ui_error_vote">Rösta i omröstning misslyckades: %1$s</string> + <string name="ui_error_accept_follow_request">Acceptera följarförfrågan misslyckades: %1$s</string> + <string name="pref_title_reading_order">Läsordning</string> + <string name="ui_error_bookmark">Att bokmärka inlägg misslyckades: %1$s</string> + <string name="ui_error_clear_notifications">Rensing av aviseringar misslyckades: %1$s</string> + <string name="ui_error_favourite">Att favoritmarkera inlägg misslyckades: %1$s</string> + <string name="ui_error_reject_follow_request">Avacceptera följarförgrågan misslyckades: %1$s</string> + <string name="label_filter_context">Filtrera sammanhang</string> + <string name="title_public_trending_hashtags">Populära hashtaggar</string> + <string name="load_newest_notifications">Ladda de nyaste aviseringarna</string> + <string name="compose_delete_draft">Ta bort utkast\?</string> + <string name="error_missing_edits">Din server vet att det här inlägget har redigerats, men har ingen kopia av ändringarna, så de kan inte visas för dig. +\n +\nDet är <a href="https://github.com/mastodon/mastodon/issues/25398">Mastodon problem #25398</a>.</string> + <string name="about_device_info_title">Din enhet</string> + <string name="about_device_info">%1$s %2$s +\nAndroidversion: %3$s +\nSDK-version: %4$d</string> + <string name="about_account_info_title">Ditt konto</string> + <string name="about_account_info">\@%1$s@%2$s +\nVersion: %3$s</string> + <string name="about_copied">Version- och enhetsinformation har kopierats</string> + <string name="about_copy">Kopiera versions- och enhetsinformation</string> + <string name="list_exclusive_label">Dölj på hem-tidslinjen</string> + <string name="dialog_delete_filter_positive_action">Radera</string> + <string name="dialog_delete_filter_text">Radera filter \'%1$s\'\?</string> + <string name="help_empty_lists">Här finns dina <b>listor</b>. Du kan skapa flera privata listor, och lägga till konton på dem. +\n +\n OBS att du bara kan lägga till konton på dina listor som du följer. +\n +\n De här listorna kan användas som en flik i Kontospecifika inställningar [iconics gmd_account_circle] [iconics gmd_navigate_next] Flikar. </string> + <string name="muting_hashtag_success_format">Tystar hasttaggen #%1$s som en varning</string> + <string name="unmuting_hashtag_success_format">Slutar tysta hashtaggen #%1$s</string> + <string name="following_hashtag_success_format">Följer hashtaggen #%1$s</string> + <string name="unfollowing_hashtag_success_format">Följer inte längre hashtaggen #%1$s</string> + <string name="error_media_playback">Kunde inte spela upp: %1$s</string> + <string name="dialog_save_profile_changes_message">Vill du spara ändringarna i din profil\?</string> + <string name="help_empty_conversations">Här finns dina <b>privata meddelanden</b>, som också kan kallas konversationer eller direktmeddelanden (DM). +\n +\nMeddelanden blir privata när ett inläggs synlighet [iconics gmd_public] ändras till [iconics gmd_mail] <i>Direkt</i>, och en eller flera användare nämns i texten. +\n +\nTill exempel kan du starta på ett kontos profil och trycka på skapa-knappen [iconics gmd_edit], och ändra synligheten. </string> + <string name="error_media_upload_sending_fmt">Uppladdningen misslyckades: %1$s</string> + <string name="title_public_trending_statuses">Populära inlägg</string> + <string name="label_image">Bild</string> + <string name="app_theme_system_black">Använd systemdesign (svart)</string> + <string name="action_view_filter">Visa filtrering</string> + <string name="error_blocking_domain">Misslyckades att tysta %1$s: %2$s</string> + <string name="error_unblocking_domain">Misslyckades att avtysta %1$s: %2$s</string> + <string name="list_reply_policy_none">Ingen</string> + <string name="list_reply_policy_list">Medlemmar av listan</string> + <string name="list_reply_policy_followed">Alla följda användare</string> + <string name="list_reply_policy_label">Visa svar till</string> + <string name="pref_title_show_self_boosts">Visa egna-knuffar</string> + <string name="pref_title_show_self_boosts_description">Någon knuffar sitt egna inlägg</string> + <string name="reply_sending_long">Ditt svar skickas.</string> + <string name="reply_sending">Skickar…</string> + <string name="pref_title_show_notifications_filter">Visa aviseringsfiltret</string> + <string name="pref_title_per_timeline_preferences">Inställningar per tidslinje</string> + <string name="action_translate">Översätt</string> + <string name="action_show_original">Visa original</string> + <string name="label_translating">Översätter…</string> + <string name="label_translated">Översatt från %1$s med %2$s</string> + <string name="ui_error_translate">Kunde inte översätta: %1$s</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-sw380dp/toot_button.xml b/app/src/main/res/values-sw380dp/toot_button.xml new file mode 100644 index 0000000..00c8bb8 --- /dev/null +++ b/app/src/main/res/values-sw380dp/toot_button.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <bool name="show_small_toot_button">false</bool> + <dimen name="toot_button_width">@dimen/wrap_content</dimen> + <dimen name="toot_button_horizontal_padding">8dp</dimen> + +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml new file mode 100644 index 0000000..84da364 --- /dev/null +++ b/app/src/main/res/values-ta/strings.xml @@ -0,0 +1,276 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">பிழை ஏற்பட்டது.</string> + <string name="error_empty">இது காலியாக இருக்க கூடாது.</string> + <string name="error_invalid_domain">தவறான டொமைன் உள்ளிடப்பட்டுள்ளது</string> + <string name="error_failed_app_registration">அந்த instance(களத்தினை)-யை அங்கீகரிப்பதில் தோல்வி.</string> + <string name="error_no_web_browser_found">வலை உலாவிகள் ஏதுமில்லை</string> + <string name="error_authorization_unknown">அடையாளம் தெரியாத அங்கீகார பிழை ஏற்பட்டுள்ளது.</string> + <string name="error_authorization_denied">அங்கீகாரம் மறுக்கப்பட்டுள்ளது</string> + <string name="error_retrieving_oauth_token">உள்நுழைவு டோக்கனைப் பெறுவதில் தோல்வி.</string> + <string name="error_compose_character_limit">நிலை மிக நீளமாக உள்ளது!</string> + <string name="error_media_upload_type">இந்த வகை கோப்பை பதிவேற்ற முடியாது.</string> + <string name="error_media_upload_opening">அந்த கோப்பை திறக்க முடியவில்லை.</string> + <string name="error_media_upload_permission">ஊடகத்தை படிக்க அனுமதி தேவை.</string> + <string name="error_media_download_permission">ஊடகத்தை சேமிக்க அனுமதி தேவை.</string> + <string name="error_media_upload_image_or_video">படங்களும், வீடியோக்களும் ஒரே நிலைக்கு இணைக்க இயலாது.</string> + <string name="error_media_upload_sending">பதிவேற்றம் தோல்வியுற்றது.</string> + <string name="title_home">முகப்பு</string> + <string name="title_notifications">அறிவிப்புகள்</string> + <string name="title_public_local">அருகாமயில்</string> + <string name="title_public_federated">ஒருங்கிணைந்த</string> + <string name="title_posts">பதிவுகள்</string> + <string name="title_posts_with_replies">பதிலளிக்கபட்டவை</string> + <string name="title_follows">பின்பற்றுகிறீர்</string> + <string name="title_followers">பின்பற்றுபவர்கள்</string> + <string name="title_favourites">விரும்பியவை</string> + <string name="title_mutes">ஒதுக்கப்பட்ட பயனர்கள்</string> + <string name="title_blocks">தடைசெய்யபட்ட பயனர்கள்</string> + <string name="title_follow_requests">பின்பற்ற கோரிக்கை</string> + <string name="title_edit_profile">சுயவிவரத்தை திருத்த</string> + <string name="title_drafts">வரைவுகள்</string> + <string name="post_boosted_format">%1$s மேலேற்றப்பட்டது</string> + <string name="post_sensitive_media_title">உணர்ச்சிகரமான உள்ளடக்கம்</string> + <string name="post_media_hidden_title">ஊடகம் மறைக்கப்பட்டது</string> + <string name="post_sensitive_media_directions">பார்வையிட சொடுக்கவும்</string> + <string name="post_content_warning_show_more">அதிகமாக்கு</string> + <string name="post_content_warning_show_less">கம்மியாக்கு</string> + <string name="footer_empty">இங்கு எதுவுமில்லை. புதுப்பிக்க கீழே இழுக்கவும்!</string> + <string name="notification_reblog_format">%1$s தங்களின் toot உயர்த்தப்பட்டுள்ளது</string> + <string name="notification_favourite_format">%1$s தங்களின் toot பிடித்தவையானது</string> + <string name="notification_follow_format">%1$s தங்களை பின்பற்றுகிறார்</string> + <string name="report_username_format">புகார் @%1$s</string> + <string name="report_comment_hint">கூடுதல் கருத்துரைகள்?</string> + <string name="action_quick_reply">உடனடி பதிலளி</string> + <string name="action_reply">பதிலளி</string> + <string name="action_reblog">மேலேற்று</string> + <string name="action_favourite">பிடித்தவை</string> + <string name="action_more">மேலும்</string> + <string name="action_compose">எழுது</string> + <string name="action_login">Mastodon மூலம் உள்நுழைய</string> + <string name="action_logout">வெளியேறு</string> + <string name="action_logout_confirm">நீங்கள் %1$s கணக்கிலிருந்து வெளியேற விரும்புகிறீர்களா?</string> + <string name="action_follow">பின்பற்று</string> + <string name="action_unfollow">பின்பற்றாதே</string> + <string name="action_block">தடை</string> + <string name="action_unblock">தடை நீக்கு</string> + <string name="action_hide_reblogs">மேலேற்றத்தை மறை</string> + <string name="action_show_reblogs">மேலேற்றத்தை காட்டு</string> + <string name="action_report">புகார்</string> + <string name="action_delete">நீக்கு</string> + <string name="action_retry">மீண்டும் முயற்சி</string> + <string name="action_close">மூடு</string> + <string name="action_view_profile">சுயவிவரம்</string> + <string name="action_view_preferences">முன்னுரிமைகள்</string> + <string name="action_view_favourites">விரும்பியவை</string> + <string name="action_view_mutes">ஒலி நீக்கபட்ட பயனர்கள்</string> + <string name="action_view_blocks">தடைசெய்யபட்ட பயனர்கள்</string> + <string name="action_view_follow_requests">பின்பற்ற கோரிக்கை</string> + <string name="action_view_media">ஊடகம்</string> + <string name="action_open_in_web">உலாவியில் திற</string> + <string name="action_add_media">Mediaவை இணை</string> + <string name="action_photo_take">புகைப்படம் எடு</string> + <string name="action_share">பகிர்</string> + <string name="action_mute">ஒலி நீக்கு</string> + <string name="action_unmute">ஒலிக்க செய்</string> + <string name="action_mention">குறிப்பிடு</string> + <string name="action_hide_media">மீடியாவை மறை</string> + <string name="action_open_drawer">டிராயரைத் திற</string> + <string name="action_save">சேமி</string> + <string name="action_edit_profile">சுயவிவரத்தை திருத்த</string> + <string name="action_edit_own_profile">திருத்த</string> + <string name="action_undo">மீளமை</string> + <string name="action_accept">ஏற்கவும்</string> + <string name="action_reject">நிராகரி</string> + <string name="action_search">தேடு</string> + <string name="action_access_drafts">வரைவுகள்</string> + <string name="action_toggle_visibility">Toot புலப்படும் தன்மை</string> + <string name="action_content_warning">உள்ளடக்க எச்சரிக்கை</string> + <string name="action_emoji_keyboard">Emoji விசைபலகை</string> + <string name="download_image">பதிவிறக்கப்படுகிறது %1$s</string> + <string name="action_copy_link">இணைப்பை நகலெடுக்கவும்</string> + <string name="send_post_link_to">Toot URL-யை பகிர…</string> + <string name="send_post_content_to">Toot உள்ளடக்கத்தை பகிர…</string> + <string name="send_media_to">Mediaவை பகிர…</string> + <string name="confirmation_reported">அனுப்பு!</string> + <string name="confirmation_unblocked">பயனர் முடக்கம் நீக்கப்பட்டது</string> + <string name="confirmation_unmuted">பயனர் ஒலிக்க செய்யபட்டது</string> + <string name="hint_domain">எந்த instance(களம்)?</string> + <string name="hint_compose">என்ன நடக்கிறது?</string> + <string name="hint_content_warning">உள்ளடக்க எச்சரிக்கை</string> + <string name="hint_display_name">காட்சி பெயர்</string> + <string name="hint_note">சுயவிவரம்</string> + <string name="hint_search">தேடல்…</string> + <string name="search_no_results">முடிவுகள் ஏதுமில்லை</string> + <string name="label_quick_reply">பதிலளி…</string> + <string name="label_avatar">தோற்றம்</string> + <string name="label_header">தலைப்பு</string> + <string name="link_whats_an_instance">Instance(களம்) என்றால் என்ன?</string> + <string name="login_connection">இணைக்கபடுகிறது…</string> + <string name="dialog_whats_an_instance">ஏதேனும் instance-ன் முகவரியையோ அல்லது களத்தின் முகவரியையோ இங்கு உள்ளிடவும், உதாரணமாக mastodon.social, icosahedron.website, social.tchncs.de, மற்றும் + <a href="https://instances.social">மேலும்!</a> + \n\nபயனர் கணக்கு இல்லையெனில் புதிய கணக்கிற்கான instance(களம்)-னை பதிவிடவும். நீங்கள் குறிப்பிடப்படும் களத்தில் உங்கள் கணக்கு பதிவாகும்.\n\nமேலும் இங்கு குறிப்பிடப்பட்ட ஏதேனும் ஒரு களத்தில் மட்டுமே உங்களால் கணக்கு ஆரம்பித்துக்கொள்ள இயலும் இருப்பினும் நம்மால் மற்ற களங்களில் உள்ள நண்பர்களையும் தொடர்பு கொள்ள இயலும் . + \n\nமேலும் தகவல்கள் அறிய <a href="https://joinmastodon.org">joinmastodon.org</a>. + </string> + <string name="dialog_title_finishing_media_upload">மீடியா பதிவேற்றம் முடிகிறது</string> + <string name="dialog_message_uploading_media">ஏற்றுகிறது …</string> + <string name="dialog_download_image">பதிவிறக்க</string> + <string name="dialog_unfollow_warning">இந்த கணக்கை பின்பற்ற வேண்டாமா?</string> + <string name="visibility_public">அனைவருக்கும் காண்பி</string> + <string name="visibility_unlisted">அனைவருக்கும் காட்டாதே</string> + <string name="visibility_private">பின்பற்றுபவர்களுக்கு மட்டும் காண்பி</string> + <string name="visibility_direct">குறிபிடபட்டுள்ள பயனர்களுக்கு மட்டும் காண்பி</string> + <string name="pref_title_edit_notification_settings">அறிவிப்புகள்</string> + <string name="pref_title_notifications_enabled">அறிவிப்புகள்</string> + <string name="pref_title_notification_alerts">எச்சரிக்கைகள்</string> + <string name="pref_title_notification_alert_sound">ஒலி மூலம் தெரிவிக்கவும்</string> + <string name="pref_title_notification_alert_vibrate">அதிர்வுடன் தெரிவிக்கவும்</string> + <string name="pref_title_notification_alert_light">ஒளியுடன் தெரிவிக்கவும்</string> + <string name="pref_title_notification_filters">எனக்கு தெரிவி எப்போதெனில்</string> + <string name="pref_title_notification_filter_mentions">என்னை குறிபிடும்போது</string> + <string name="pref_title_notification_filter_follows">என்னை பின்பற்றினால்</string> + <string name="pref_title_notification_filter_reblogs">என் பதிவுகள் அதிகரிக்கப்பட்டால்</string> + <string name="pref_title_notification_filter_favourites">என் பதிவுகள் பிடித்தவையானால்</string> + <string name="pref_title_appearance_settings">தோற்றம்</string> + <string name="pref_title_app_theme">செயலியின் தீம்</string> + <string name="app_them_dark">கருமை</string> + <string name="app_theme_light">வெளிச்சம்</string> + <string name="app_theme_black">பிளாக்</string> + <string name="app_theme_auto">தானியங்கி</string> + <string name="pref_title_browser_settings">உலாவி</string> + <string name="pref_title_custom_tabs">Chrome தனிப்பயன் கீற்றை பயன்படுத்து</string> + <string name="pref_title_post_filter">காலவரிசை வடிகட்டல்</string> + <string name="pref_title_post_tabs">கீற்றுகள்</string> + <string name="pref_title_show_boosts">மேலேற்றத்தை காண்பி</string> + <string name="pref_title_show_replies">பதில்களைக் காண்பி</string> + <string name="pref_title_show_media_preview">ஊடக மாதிரிக்காட்சிகளைக் காண்பி</string> + <string name="pref_title_proxy_settings">ப்ராக்ஸி</string> + <string name="pref_title_http_proxy_settings">HTTP ப்ராக்ஸி</string> + <string name="pref_title_http_proxy_enable">HTTP ப்ராக்ஸியை இயக்கு</string> + <string name="pref_title_http_proxy_server">HTTP ப்ராக்ஸி சேவையகம்</string> + <string name="pref_title_http_proxy_port">HTTP ப்ராக்ஸி போர்ட்</string> + <string name="pref_default_post_privacy">தனியுரிமையில் பதிவிடுவதை முன்னிருப்பாக்கு</string> + <string name="pref_publishing">வெளியீடு</string> + <string name="post_privacy_public">அனைவருக்கும்</string> + <string name="post_privacy_unlisted">பட்டியலிடப்படாதவர்களுக்கு</string> + <string name="post_privacy_followers_only">பின்பற்றுபவர்களுக்கு மட்டும்</string> + <string name="pref_post_text_size">நிலை உரை அளவு</string> + <string name="notification_mention_name">புதிய குறிப்புகள்</string> + <string name="notification_mention_descriptions">புதிய குறிப்புகள் பற்றிய அறிவிப்புகள்</string> + <string name="notification_follow_name">புதிய பின்பற்றுபவர்கள்</string> + <string name="notification_follow_description">புதிய பின்தொடர்பவர்களைப் பற்றிய அறிவிப்புகள்</string> + <string name="notification_boost_name">ஊக்கியாக</string> + <string name="notification_boost_description">எமது toot மேலேற்றும் போது தெரிவி.</string> + <string name="notification_favourite_name">பிடித்தவைகள்</string> + <string name="notification_favourite_description">எமது toot பிடித்தவையானால் தெரிவி.</string> + <string name="notification_mention_format">%1$s தங்களை குறிபிட்டுள்ளார்</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s மற்றும் %4$d மற்றவர்கள்</string> + <string name="notification_summary_medium">%1$s, %2$s, மற்றும் %3$s</string> + <string name="notification_summary_small">%1$s மற்றும் %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="other">%1$d புதிய ஊடாடுதல்</item> + </plurals> + <string name="description_account_locked">கணக்கு மூடப்பட்டது</string> + <string name="about_title_activity">பற்றி</string> + <string name="about_tusky_version">Tusky(டஸ்கி) %1$s</string> + <string name="about_tusky_license">Tusky ஒரு கட்டற்ற மற்றும் திறந்த மூல மென்பொருள். இதன் உரிமம் GNU General Public License(பொது உரிமம்) பதிப்பு 3 -ன் கீழ் உள்ளது. நீங்கள் உரிமம் பற்றி காண: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site"> திட்டத்தின் வலைத்தளம்:\n + https://tusky.app + </string> + <string name="about_bug_feature_request_site"> பிழை அறிக்கைகள் & அம்ச கோரிக்கைகள்:\n + https://github.com/tuskyapp/Tusky/issues + </string> + <string name="about_tusky_account">Tusky-ன் கணக்கு</string> + <string name="post_share_content">Toot உள்ளடக்கத்தைப் பகிர்</string> + <string name="post_share_link">Toot இணைப்பைப் பகிர்</string> + <string name="post_media_images">படங்கள்</string> + <string name="post_media_video">காணொளி</string> + <string name="state_follow_requested">கோரிக்கையைப் பின்பற்றவும்</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">%1$dஆ-முன்</string> + <string name="abbreviated_in_days">%1$dநா-முன்</string> + <string name="abbreviated_in_hours">%1$dம.நே-முன்</string> + <string name="abbreviated_in_minutes">%1$dநி-முன்</string> + <string name="abbreviated_in_seconds">%1$dவி-முன்</string> + <string name="abbreviated_years_ago">%1$dஆ</string> + <string name="abbreviated_days_ago">%1$dநா</string> + <string name="abbreviated_hours_ago">%1$dம.நே</string> + <string name="abbreviated_minutes_ago">%1$dநி</string> + <string name="abbreviated_seconds_ago">%1$dவி</string> + <string name="follows_you">நீங்கள் பின் தொடரபடுகிறீர்கள்</string> + <string name="pref_title_alway_show_sensitive_media">எல்லா nsfw(உணர்ச்சிகரமான) உள்ளடக்கத்தையும் எப்போதும் காண்பி</string> + <string name="title_media">ஊடகம்</string> + <string name="replying_to">\@%1$s -க்கு பதிலளி</string> + <string name="load_more_placeholder_text">மேலும் காட்டு…</string> + <string name="add_account_name">கணக்கை சேர்க்க</string> + <string name="add_account_description">புதிய Mastodon கணக்கைச் சேர்க்க</string> + <string name="action_lists">பட்டியல்கள்</string> + <string name="title_lists">பட்டியல்கள்</string> + <string name="compose_active_account_description">%1$s கணக்குடன் பதிவிட</string> + <string name="action_set_caption">தலைப்பை அமை</string> + <string name="action_remove">நீக்கு</string> + <string name="lock_account_label">கணக்கை முடக்கு</string> + <string name="lock_account_label_description">நீங்களாக பின்பற்றுபவர்களை அங்கீகரிக்க</string> + <string name="compose_save_draft">வரைவை சேமிக்கவா?</string> + <string name="send_post_notification_title">Toot அனுப்பபடுகிறது…</string> + <string name="send_post_notification_error_title">Toot அனுப்புவதில் பிழை</string> + <string name="send_post_notification_channel_name">Toots அனுப்பபடுகிறது</string> + <string name="send_post_notification_cancel_title">Toot அனுப்பபல் நீக்கபட்டது</string> + <string name="send_post_notification_saved_content">நகலெடுக்கபட்ட toot வரைவில் சேமிக்கபட்டது</string> + <string name="action_compose_shortcut">எழுது</string> + <string name="error_no_custom_emojis">தங்கள் %1$s instance(களம்)-ல் எந்தவொரு custom emojis-ம் இல்லை </string> + <string name="emoji_style">Emoji பாணி</string> + <string name="system_default">அமைப்பின் இயல்புநிலை</string> + <string name="download_fonts">தாங்கள் முதலில் இந்த Emoji sets-னை பதிவிறக்கவேண்டும்</string> + <string name="performing_lookup_title">சேயல்பாட்டு தேடல்…</string> + <string name="expand_collapse_all_posts">அதிகமாக்கு/கம்மியாக்கு பற்றிய நிலைகள்</string> + <string name="action_open_post">Tootயை திற</string> + <string name="restart_required">செயலி மறுதொடக்கம் தேவைபடுகிறது</string> + <string name="restart_emoji">இந்த மாறுதல்கள் செயற்படுத்த செயலியை மறுதொடக்கம் செய்ய வேண்டும்</string> + <string name="later">பிறகு</string> + <string name="restart">மறுதொடக்க</string> + <string name="caption_systememoji">தங்களின் வழக்கமான emoji பொருத்தப்பட்டது</string> + <string name="caption_blobmoji">Android 4.4–7.1-லிருந்தே Blob emojis பயன்படுத்தபடுகிறது</string> + <string name="caption_twemoji">Mastodon-னின் வழக்கமான emoji set</string> + <string name="download_failed">பதிவிறக்கம் தோல்வியுற்றது</string> + <string name="profile_badge_bot_text">இயலி</string> + <string name="account_moved_description">%1$s கணக்கு நகர்த்தபட்டது இதற்க்கு:</string> + <string name="reblog_private">அசலான பார்வையாளர்களுக்கு மட்டும் மேலேற்று</string> + <string name="unreblog_private">மேலேற்றத்தை தவிர்</string> + <string name="license_description">Tusky கொண்டுள்ள நிரல் மற்றும் துணுக்குகள் பின்வரும் திறந்த மூல திட்டங்கள்:</string> + <string name="license_apache_2">Apache License (copy below)-ன் கீழ் உரிமமளிக்கப்பட்டுள்ளது</string> + <string name="profile_metadata_label">சுயவிவர மேனிலை தரவு</string> + <string name="profile_metadata_add">தகவலை இணைக்க</string> + <string name="profile_metadata_label_label">விவரத்துணுக்கு</string> + <string name="profile_metadata_content_label">உள்ளடக்கம்</string> + <string name="pref_title_absolute_time">Absolute நேரத்தை பயன்படுத்து</string> + <string name="label_remote_account">கீழுள்ள தகவல் பயனரின் சுயவிவரத்தின் பிரதிபலிப்பு முழுமையடையாது. முழு சுயவிவரத்தை உலாவில் திறக்க அழுத்தவும்.</string> + <string name="unpin_action">விடுவி</string> + <string name="pin_action">பொருத்து</string> + <string name="action_view_account_preferences">கணக்கரின் முன்னுரிமைகள்</string> + <string name="error_network">பிணைய பிழை ஏற்பட்டது! உங்கள் இணைப்பைச் சரிபார்த்து மீண்டும் முயற்சிக்கவும்!</string> + <string name="error_sender_account_gone">டூத் அனுப்புவதில் பிழை ஏற்பட்டுள்ளது</string> + <string name="title_direct_messages">நேரடி தகவல்</string> + <string name="title_tab_preferences">பட்டைகள்</string> + <string name="title_posts_pinned">பொருத்தப்பட்டது</string> + <string name="duration_1_day">1 நாள்</string> + <string name="duration_3_days">3 நாட்கள்</string> + <string name="duration_7_days">7 நாட்கள்</string> + <string name="add_poll_choice">விருப்பத்தைச் சேர்</string> + <string name="notification_follow_request_name">பின்பற்ற கோரிக்கை</string> + <string name="filter_dialog_remove_button">நீக்கு</string> + <string name="action_open_reblogged_by">மேலேற்றத்தை காண்பி</string> + <string name="edit_poll">திருத்த</string> + <string name="action_edit">திருத்த</string> + <string name="conversation_2_recipients">%1$s மற்றும் %2$s</string> + <string name="description_visibility_private">பின்பற்றுபவர்கள்</string> + <string name="description_visibility_unlisted">பட்டியலிடப்படாதவர்களுக்கு</string> + <string name="description_visibility_public">அனைவருக்கும்</string> + <string name="compose_shortcut_short_label">எழுது</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-te/strings.xml b/app/src/main/res/values-te/strings.xml new file mode 100644 index 0000000..a6b3dae --- /dev/null +++ b/app/src/main/res/values-te/strings.xml @@ -0,0 +1,2 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources></resources> \ No newline at end of file diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml new file mode 100644 index 0000000..011841a --- /dev/null +++ b/app/src/main/res/values-th/strings.xml @@ -0,0 +1,481 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="add_poll_choice">เพิ่มตัวเลือก</string> + <string name="duration_7_days">7 วัน</string> + <string name="duration_3_days">3 วัน</string> + <string name="duration_1_day">1 วัน</string> + <string name="duration_6_hours">6 ชั่วโมง</string> + <string name="duration_1_hour">1 ชั่วโมง</string> + <string name="duration_30_min">30 นาที</string> + <string name="duration_5_min">5 นาที</string> + <string name="create_poll_title">โพล</string> + <string name="pref_title_enable_swipe_for_tabs">เปิดใช้งานการเลื่อนนิ้วเพื่อสลับระหว่างแท็บ</string> + <string name="failed_search">ค้นหาล้มเหลว</string> + <string name="title_accounts">บัญชี</string> + <string name="report_description_remote_instance">บัญชีนี้มาจากเซิร์ฟเวอร์อื่น ส่งสำเนารายงานที่ไม่ระบุชื่อไปที่นั่นด้วยหรือไม่\?</string> + <string name="report_description_1">รายงานจะถูกส่งไปยังผู้ดูแลเซิร์ฟเวอร์ของคุณ สามารถให้คำอธิบายว่าทำไมจึงรายงานบัญชีนี้ด้านล่าง:</string> + <string name="failed_fetch_posts">ดึงข้อมูลสถานะล้มเหลว</string> + <string name="failed_report">รายงานล้มเหลว</string> + <string name="report_remote_instance">ส่งต่อไปยัง %1$s</string> + <string name="hint_additional_info">ความคิดเห็นเพิ่มเติม</string> + <string name="report_sent_success">รายงาน @%1$s เรียบร้อยแล้ว</string> + <string name="button_done">ทำ</string> + <string name="button_back">ย้อนกลับ</string> + <string name="button_continue">ต่อไป</string> + <plurals name="poll_timespan_seconds"> + <item quantity="other">เหลืออีก %1$d วินาที</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="other">เหลืออีก %1$d นาที</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="other">เหลืออีก %1$d ชั่วโมง</item> + </plurals> + <plurals name="poll_timespan_days"> + <item quantity="other">เหลืออีก %1$d วัน</item> + </plurals> + <string name="poll_ended_created">โพลที่คุณสร้างสิ้นสุดลงแล้ว</string> + <string name="poll_ended_voted">โพลที่คุณโหวตสิ้นสุดลงแล้ว</string> + <string name="poll_vote">โหวต</string> + <string name="poll_info_closed">สิ้นสุดแล้ว</string> + <string name="poll_info_time_absolute">จบที่ %1$s</string> + <plurals name="poll_info_people"> + <item quantity="other">%1$s คน</item> + </plurals> + <plurals name="poll_info_votes"> + <item quantity="other">%1$s โหวต</item> + </plurals> + <string name="poll_info_format"> <!-- 15 votes • 1 hour left --> %1$s • %2$s</string> + <string name="compose_preview_image_description">การกระทำสำหรับภาพ %1$s</string> + <string name="notification_clear_text">ต้องการลบการแจ้งเตือนทั้งหมดอย่างสมบูรณ์\?</string> + <string name="compose_shortcut_short_label">เขียน</string> + <string name="compose_shortcut_long_label">เขียน Toot</string> + <string name="filter_apply">ใช้งาน</string> + <string name="notifications_apply_filter">คัดกรอง</string> + <string name="notifications_clear">ล้างการแจ้งเตือน</string> + <string name="list">รายการ</string> + <string name="select_list_title">เลือกรายการ</string> + <string name="hashtags">แฮชแท็ก</string> + <string name="edit_hashtag_hint">แฮชแท็กโดยไม่มี #</string> + <string name="add_hashtag_title">เพิ่มแฮชแท็ก</string> + <string name="hint_list_name">ชื่อรายการ</string> + <string name="description_poll">โพลกับตัวเลือก: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="description_visibility_direct">ไดเร็กต์</string> + <string name="description_visibility_private">ผู้ติดตาม</string> + <string name="description_visibility_unlisted">ไม่อยู่ในรายการ</string> + <string name="description_visibility_public">สาธารณะ</string> + <string name="description_post_bookmarked">คั่นหน้า</string> + <string name="description_post_favourited">ชื่นชอบ</string> + <string name="description_post_reblogged">ได้ถูกเขียนใหม่</string> + <string name="description_post_media_no_description_placeholder">ไม่มีคำอธิบาย</string> + <string name="description_post_cw">เตือนเนื้อหา : %1$s</string> + <string name="description_post_media">สื่อ: %1$s</string> + <string name="conversation_more_recipients">%1$s, %2$s และอีก %3$d</string> + <string name="conversation_2_recipients">%1$s และ %2$s</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="title_favourited_by">ชื่นชอบโดย</string> + <string name="title_reblogged_by">บูสต์โดย</string> + <plurals name="reblogs"> + <item quantity="other"><b>%1$s</b> บูสต์</item> + </plurals> + <plurals name="favs"> + <item quantity="other"><b>%1$s</b> ชื่นชอบ</item> + </plurals> + <string name="pin_action">ปักหมุด</string> + <string name="unpin_action">เลิกปักหมุด</string> + <string name="label_remote_account">ข้อมูลต่อไปนี้อาจไม่ถูกต้อง แตะเพื่อเปิดโปรไฟล์ในเบราว์เซอร์</string> + <string name="pref_title_absolute_time">แสดงเวลาแบบเที่ยงตรง</string> + <string name="profile_metadata_content_label">เนื้อหา</string> + <string name="profile_metadata_label_label">ป้าย</string> + <string name="profile_metadata_add">เพิ่มข้อมูล</string> + <string name="profile_metadata_label">ข้อมูลอภิพันธุ์</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_apache_2">ภายใต้สัญญาอนุญาต Apache License (คัดลอกด้านล่าง)</string> + <string name="license_description">Tusky มีโค้ดและสินทรัพย์จากโครงการโอเพนซอร์สต่อไปนี้:</string> + <string name="unreblog_private">ยกเลิกบูสต์</string> + <string name="reblog_private">บูสต์โพสต์ต้นฉบับ</string> + <string name="account_moved_description">%1$s ได้ย้ายไปที่ :</string> + <string name="profile_badge_bot_text">บอต</string> + <string name="download_failed">ดาวน์โหลดล้มเหลว</string> + <string name="caption_notoemoji">ชุดเอโมจิปัจจุบันจากกูเกิล</string> + <string name="caption_twemoji">ชุดเอโมจิจาก Mastodon</string> + <string name="caption_blobmoji">ที่รู้จักจาก Android 4.4 ถึง 7.1 ชุดเอโมจิ Blob</string> + <string name="caption_systememoji">ชุดเริ่มต้นในอุปกรณ์คุณ</string> + <string name="restart">เริ่มใหม่</string> + <string name="later">ภายหลัง</string> + <string name="restart_emoji">จำเป็นต้องเริ่ม Tusky ใหม่ เพื่อใช้การเปลี่ยนแปลงเหล่านี้</string> + <string name="restart_required">จำเป็นต้องเริ่มแอปใหม่</string> + <string name="action_open_post">เปิด Toot</string> + <string name="expand_collapse_all_posts">ขยาย/ย่อทั้งหมด</string> + <string name="performing_lookup_title">กำลังค้นหา…</string> + <string name="download_fonts">ต้องดาวน์โหลดชุดเอโมจิเหล่านี้ก่อน</string> + <string name="system_default">ค่าปริยายของระบบ</string> + <string name="emoji_style">รูปแบบเอโมจิ</string> + <string name="error_no_custom_emojis">Instance %1$s ไม่มีเอโมจิแบบกำหนดเอง</string> + <string name="action_compose_shortcut">เขียน</string> + <string name="send_post_notification_saved_content">สำเนา Toot บันทึกเป็นฉบับร่างแล้ว</string> + <string name="send_post_notification_cancel_title">การส่งถูกยกเลิก</string> + <string name="send_post_notification_channel_name">ส่ง Toot</string> + <string name="send_post_notification_error_title">การส่ง Toot เกิดข้อผิดผลาด</string> + <string name="send_post_notification_title">กำลังส่ง Toot…</string> + <string name="compose_save_draft">บันทึกฉบับร่าง\?</string> + <string name="lock_account_label_description">ต้องอนุมัติผู้ติดตามด้วยตัวเอง</string> + <string name="lock_account_label">ล็อกบัญชี</string> + <string name="action_remove">ลบ</string> + <string name="action_set_caption">ตั้งคำอธิบาย</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">อธิบายเพื่อผู้บกพร่องทางสายตา +\n(จำกัด %1$d ตัวอักขระ)</item> + </plurals> + <string name="compose_active_account_description">โพสต์ด้วยบัญชี %1$s</string> + <string name="action_remove_from_list">ลบบัญชีออกจากรายการ</string> + <string name="action_add_to_list">เพิ่มบัญชีไปใส่รายการ</string> + <string name="hint_search_people_list">ค้นหาผู้ติดตาม</string> + <string name="action_delete_list">ลบรายการ</string> + <string name="action_rename_list">เปลี่ยนชื่อรายการ</string> + <string name="action_create_list">สร้างรายการ</string> + <string name="error_delete_list">ไม่สามารถลบรายการได้</string> + <string name="error_rename_list">ไม่สามารถเปลี่ยนชื่อรายการได้</string> + <string name="error_create_list">ไม่สามารถสร้างรายการได้</string> + <string name="add_account_description">เพิ่มบัญชี Mastodon ใหม่</string> + <string name="add_account_name">เพิ่มบัญชี</string> + <string name="filter_add_description">วลีที่ต้องการกรอง</string> + <string name="filter_dialog_whole_word_description">ถ้าคำหลักหรือวลีเป็นอักษรผสมตัวเลข จะใช้ได้ผลเมื่อตรงทั้งคำเท่านั้น</string> + <string name="filter_dialog_whole_word">ทั้งคำ</string> + <string name="filter_dialog_update_button">อัปเดต</string> + <string name="filter_dialog_remove_button">ลบ</string> + <string name="filter_edit_title">แก้ไขตัวคัดกรอง</string> + <string name="filter_addition_title">เพิ่มตัวคัดกรอง</string> + <string name="pref_title_thread_filter_keywords">การสนทนา</string> + <string name="pref_title_public_filter_keywords">ไทม์ไลน์สาธารณะ</string> + <string name="load_more_placeholder_text">โหลดเพิ่ม</string> + <string name="replying_to">ตอบกลับไป @%1$s</string> + <string name="title_media">สื่อ</string> + <string name="pref_title_alway_open_spoiler">ขยาย Toot ที่มีเครื่องหมายเนื้อหาอ่อนไหวเสมอ</string> + <string name="pref_title_alway_show_sensitive_media">แสดงเนื้อหาอ่อนไหวเสมอ</string> + <string name="follows_you">กำลังติดตามคุณ</string> + <string name="abbreviated_seconds_ago">%1$d วินาทีที่แล้ว</string> + <string name="abbreviated_minutes_ago">%1$d นาทีที่แล้ว</string> + <string name="abbreviated_hours_ago">%1$d ชั่วโมงที่แล้ว</string> + <string name="abbreviated_days_ago">%1$d วันที่แล้ว</string> + <string name="abbreviated_years_ago">%1$d ปีที่แล้ว</string> + <string name="abbreviated_in_seconds">ใน %1$d วินาที</string> + <string name="abbreviated_in_minutes">ใน %1$d นาที</string> + <string name="abbreviated_in_hours">ใน %1$d ชั่วโมง</string> + <string name="abbreviated_in_days">ใน %1$d วัน</string> + <string name="abbreviated_in_years">ใน %1$d ปี</string> + <string name="state_follow_requested">กำลังขอติดตาม</string> + <string name="post_media_video">วิดีทัศน์</string> + <string name="post_media_images">ภาพ</string> + <string name="post_share_link">แบ่งปันลิงก์ Toot</string> + <string name="post_share_content">แบ่งปันเนื้อหา Toot</string> + <string name="about_tusky_account">บัญชีทางการของ Tusky</string> + <string name="about_bug_feature_request_site">รายงานช่องโหว่ และ ขอฟีเจอร์ (ภาษาอังกฤษ): +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_project_site">เว็บไซต์โปรเจกต์: +\nhttps://tusky.app</string> + <string name="about_tusky_license">Tusky คือซอฟต์แวร์เสรีและโอเพนซอร์ส ภายใต้สัญญาอนุญาต GNU General Public License Version 3 ดูสัญญาที่ : https://www.gnu.org/licenses/gpl-3.0.ja.html</string> + <string name="about_powered_by_tusky">ขับเคลื่อนด้วย Tusky</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="description_account_locked">บัญชีไม่สาธารณะ</string> + <plurals name="notification_title_summary"> + <item quantity="other">การโต้ตอบใหม่จำนวน %1$d</item> + </plurals> + <string name="notification_summary_small">%1$s และ %2$s</string> + <string name="notification_summary_medium">%1$s, %2$s, และ %3$s</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s และอีก %4$d คน</string> + <string name="notification_mention_format">%1$s ตอบกลับคุณ</string> + <string name="notification_poll_description">การแจ้งเตือนเมื่อโพลได้สิ้นสุดลงแล้ว</string> + <string name="notification_poll_name">โพล</string> + <string name="notification_favourite_description">การแจ้งเตือนเมื่อ Toot คุณถูกชื่นชอบ</string> + <string name="notification_favourite_name">ชื่นชอบ</string> + <string name="notification_boost_description">การแจ้งเตือนเมื่อ Toot คุณถูกบูสต์</string> + <string name="notification_boost_name">บูสต์</string> + <string name="notification_follow_request_description">การแจ้งเตือนคำขอติดตามใหม่</string> + <string name="notification_follow_request_name">คำขอติดตาม</string> + <string name="notification_follow_description">การแจ้งเตือนเกี่ยวกับผู้ติดตามใหม่</string> + <string name="notification_follow_name">ผู้ติดตามใหม่</string> + <string name="notification_mention_name">การกล่าวถึงใหม่</string> + <string name="notification_mention_descriptions">การแจ้งเตือนเกี่ยวกับการกล่าวถึงใหม่</string> + <string name="post_text_size_largest">ใหญ่มาก</string> + <string name="post_text_size_large">ใหญ่</string> + <string name="post_text_size_medium">กลาง</string> + <string name="post_text_size_small">เล็ก</string> + <string name="post_text_size_smallest">เล็กมาก</string> + <string name="pref_post_text_size">ขนาดอักษร Toot</string> + <string name="post_privacy_followers_only">เฉพาะผู้ติดตาม</string> + <string name="post_privacy_unlisted">ไม่อยู่ในรายการ</string> + <string name="post_privacy_public">สาธารณะ</string> + <string name="pref_failed_to_sync">ซิงค์การตั้งค่าล้มเหลว</string> + <string name="pref_publishing">กำลังเผยแพร่ (synced with server)</string> + <string name="pref_default_media_sensitivity">ใส่เครื่องหมายว่าเป็นสื่ออ่อนไหวเสมอ</string> + <string name="pref_default_post_privacy">ค่าปริยายความเป็นส่วนตัวของโพสต์</string> + <string name="pref_title_http_proxy_port">พอร์ตพร็อกซี่ HTTP</string> + <string name="pref_title_http_proxy_server">เซิร์ฟเวอร์พร็อกซี่ HTTP</string> + <string name="pref_title_http_proxy_enable">เปิดใช้งานพร็อกซี่ HTTP</string> + <string name="pref_title_http_proxy_settings">พร็อกซี่ HTTP</string> + <string name="pref_title_proxy_settings">พร็อกซี่</string> + <string name="pref_title_show_media_preview">ดาวน์โหลดตัวอย่างสื่อ</string> + <string name="pref_title_show_replies">แสดงการตอบกลับ</string> + <string name="pref_title_show_boosts">แสดงบูสต์</string> + <string name="pref_title_post_tabs">แท็บ</string> + <string name="pref_title_post_filter">คัดกรองไทม์ไลน์</string> + <string name="pref_title_animate_gif_avatars">อวตาร GIF เคลื่อนไหวได้</string> + <string name="pref_title_bot_overlay">แสดงสัญลักษณ์ว่าเป็นบอต</string> + <string name="pref_title_language">ภาษา</string> + <string name="pref_title_custom_tabs">ใช้ Chrome Custom Tabs</string> + <string name="pref_title_browser_settings">เบราว์เซอร์</string> + <string name="app_theme_system">ใช้ตามแบบระบบ</string> + <string name="app_theme_auto">ปรับตามเวลา</string> + <string name="app_theme_black">ดำ</string> + <string name="app_theme_light">สว่าง</string> + <string name="app_them_dark">มืด</string> + <string name="pref_title_timeline_filters">คัดกรอง</string> + <string name="pref_title_timelines">ไทม์ไลน์</string> + <string name="pref_title_app_theme">ธีมแอป</string> + <string name="pref_title_appearance_settings">ลักษณะ</string> + <string name="pref_title_notification_filter_poll">โพลสิ้นสุดแล้ว</string> + <string name="pref_title_notification_filter_favourites">โพสต์ถูกชื่นชอบ</string> + <string name="pref_title_notification_filter_reblogs">โพสต์ถูกบูสต์</string> + <string name="pref_title_notification_filter_follow_requests">คำขอติดตาม</string> + <string name="pref_title_notification_filter_follows">ติดตาม</string> + <string name="pref_title_notification_filter_mentions">กล่าวถึง</string> + <string name="pref_title_notification_filters">แจ้งฉันเมื่อ</string> + <string name="pref_title_notification_alert_sound">แจ้งด้วยเสียง</string> + <string name="pref_title_notification_alert_vibrate">แจ้งด้วยการสั่น</string> + <string name="pref_title_notification_alert_light">แจ้งด้วยแสง</string> + <string name="pref_title_notification_alerts">เตือน</string> + <string name="pref_title_notifications_enabled">การแจ้งเตือนแบบ Push</string> + <string name="pref_title_edit_notification_settings">ตั้งค่าการแจ้งเตือน</string> + <string name="visibility_direct">ไดเร็กต์:โพสต์ให้เฉพาะผู้ที่ถูกกล่าวถึงเห็น</string> + <string name="visibility_private">เฉพาะผู้ติดตาม:โพสต์ให้เฉพาะผู้ติดตามเห็น</string> + <string name="visibility_unlisted">ไม่อยู่ในรายการ:ไม่แสดงในไทม์ไลน์สาธารณะ</string> + <string name="visibility_public">สาธารณะ:โพสต์ในไทม์ไลน์สาธารณะ</string> + <string name="dialog_mute_warning">ปิดเสียง @%1$s\?</string> + <string name="dialog_block_warning">บล็อก @%1$s\?</string> + <string name="mute_domain_warning_dialog_ok">ซ่อนทั้งโดเมน</string> + <string name="mute_domain_warning">ต้องการบล็อกทุกอย่างจาก %1$s \? คุณจะไม่เห็นเนื้อหาจากโดเมนนั้นในไทม์ไลน์สาธารณะหรือในการแจ้งเตือน ผู้ติดตามของคุณจากโดเมนนั้นจะถูกลบออก</string> + <string name="dialog_redraft_post_warning">ลบ แล้ว ร่าง Toot นี้ใหม่\?</string> + <string name="dialog_delete_post_warning">ลบ Toot นี้\?</string> + <string name="dialog_unfollow_warning">เลิกติดตามผู้ใช้นี้\?</string> + <string name="dialog_message_cancel_follow_request">ยกเลิกคำขอติดตาม\?</string> + <string name="dialog_download_image">ดาวน์โหลด</string> + <string name="dialog_message_uploading_media">กำลังอัปโหลด…</string> + <string name="dialog_title_finishing_media_upload">กำลังอัปโหลดสื่อใกล้เสร็จ</string> + <string name="dialog_whats_an_instance">ใส่ที่อยู่หรือโดเมนของ Instance ได้ที่นี่ เช่น mastodon.social icosahedron.website social.tchncs.de และ <a href="https://instances.social">อีกมากมาย!</a> +\n +\nถ้ายังไม่มีบัญชี สามารถใส่ชื่อ Instance ที่ต้องการจะร่วมแล้วสร้างบัญชีที่นั่น +\n +\nInstance คือที่ที่หนึ่งไว้โฮสต์บัญชีคุณ แต่คุณยังสามารถสื่อสาร ติดตามบุคคลบน Instance อื่นได้เหมือนอยู่บนไซต์เดียวกัน +\n +\nพบข้อมูลเพิ่มเติมได้ที่ <a href="https://joinmastodon.org">joinmastodon.org</a> </string> + <string name="label_header">ภาพหัวบน</string> + <string name="login_connection">กำลังเชื่อมต่อ…</string> + <string name="label_avatar">อวตาร</string> + <string name="label_quick_reply">ตอบกลับ…</string> + <string name="search_no_results">ไม่มีผลลัพธ์</string> + <string name="hint_search">ค้นหา…</string> + <string name="hint_note">ข้อมูลส่วนตัว</string> + <string name="hint_display_name">ชื่อที่ใช้แสดง</string> + <string name="hint_content_warning">คำเตือนเนื้อหา</string> + <string name="hint_compose">เกิดอะไรขึ้นเอย\?</string> + <string name="hint_domain">Instance ไหน\?</string> + <string name="confirmation_domain_unmuted">เลิกซ่อน %1$s แล้ว</string> + <string name="confirmation_unblocked">เลิกบล็อกผู้ใช้แล้ว</string> + <string name="confirmation_unmuted">เลิกปิดเสียงผู้ใช้นี้แล้ว</string> + <string name="confirmation_reported">ส่งแล้ว!</string> + <string name="send_media_to">แบ่งปันสื่อไป…</string> + <string name="send_post_content_to">แบ่งปัน Toot ไป…</string> + <string name="send_post_link_to">แชร์ URL Toot ไป…</string> + <string name="downloading_media">กำลังดาวน์โหลดสื่อ</string> + <string name="download_media">ดาวน์โหลดสื่อ</string> + <string name="action_share_as">แบ่งปันโดย…</string> + <string name="action_open_as">เปิดเป็น %1$s</string> + <string name="action_copy_link">คัดลอกลิงก์</string> + <string name="download_image">กำลังดาวน์โหลด %1$s</string> + <string name="action_open_media_n">เปิดสื่อ #%1$d</string> + <string name="title_links_dialog">ลิงก์</string> + <string name="title_mentions_dialog">โต้ตอบ</string> + <string name="title_hashtags_dialog">แฮชแท็ก</string> + <string name="action_open_faved_by">ดูชื่นชอบ</string> + <string name="action_open_reblogged_by">ดูบสต์</string> + <string name="action_open_reblogger">ดูต้นตอบูสต์</string> + <string name="action_hashtags">แฮชแท็ก</string> + <string name="action_mentions">โต้ตอบ</string> + <string name="action_links">ลิงก์</string> + <string name="action_add_tab">เพิ่มแท็บ</string> + <string name="action_schedule_post">Toot แบบตั้งเวลา</string> + <string name="action_emoji_keyboard">คีย์บอร์ดเอโมจิ</string> + <string name="action_content_warning">เตือนเนื้อหา</string> + <string name="action_toggle_visibility">การมองเห็น Toot</string> + <string name="action_access_scheduled_posts">Toot แบบตั้งเวลา</string> + <string name="action_access_drafts">ฉบับร่าง</string> + <string name="action_reject">ปฏิเสธ</string> + <string name="action_accept">ยอมรับ</string> + <string name="action_undo">ยกเลิก</string> + <string name="action_edit_own_profile">แก้ไข</string> + <string name="action_save">บันทึก</string> + <string name="action_open_drawer">เปิดเมนู</string> + <string name="action_hide_media">ซ่อนสื่อ</string> + <string name="action_mention">กล่าวถึง</string> + <string name="action_unmute_conversation">เลิกปิดเสียงการสนทนา</string> + <string name="action_mute_conversation">ปิดเสียงการสนทนานี้</string> + <string name="action_mute_domain">ปิดเสียง %1$s</string> + <string name="action_unmute">เลิกปิดเสียง</string> + <string name="action_mute">ปิดเสียง</string> + <string name="action_share">แบ่งปัน</string> + <string name="action_photo_take">ถ่ายภาพ</string> + <string name="action_add_poll">เพิ่มโพล</string> + <string name="action_add_media">เพิ่มสื่อ</string> + <string name="action_open_in_web">เปิดในเบราว์เซอร์</string> + <string name="action_view_media">สื่อ</string> + <string name="action_view_follow_requests">คำขอติดตาม</string> + <string name="action_view_domain_mutes">โดเมนที่ซ่อนไว้</string> + <string name="action_view_blocks">ผู้ใช้ที่ถูกบล็อกไว้</string> + <string name="action_view_mutes">ผู้ใช้ที่ปิดเสียงไว้</string> + <string name="action_view_bookmarks">ที่คั่นหน้า</string> + <string name="action_view_favourites">ชื่นชอบ</string> + <string name="action_view_profile">โปรไฟล์</string> + <string name="action_close">ปิด</string> + <string name="action_retry">ลองอีกครั้ง</string> + <string name="action_send_public">โพสต์!</string> + <string name="action_send">โพสต์</string> + <string name="action_delete_and_redraft">ลบแล้วร่างใหม่</string> + <string name="action_delete">ลบ</string> + <string name="action_edit">แก้ไข</string> + <string name="action_report">รายงาน</string> + <string name="action_show_reblogs">แสดงการดัน</string> + <string name="action_hide_reblogs">ซ่อนการดัน</string> + <string name="action_unblock">เลิกบล็อก</string> + <string name="action_block">บล็อก</string> + <string name="action_unfollow">เลิกติดตาม</string> + <string name="action_follow">ติดตาม</string> + <string name="action_logout_confirm">คุณต้องการออกจากระบบของบัญชี %1$s หรือไม่\?</string> + <string name="action_compose">เขียนโพสต์ใหม่</string> + <string name="action_more">อื่น ๆ</string> + <string name="action_unfavourite">เลิกชื่นชอบ</string> + <string name="action_bookmark">คั่นหน้า</string> + <string name="action_favourite">ชื่นชอบ</string> + <string name="action_unreblog">ลบการดัน</string> + <string name="action_reblog">ดัน</string> + <string name="action_reply">ตอบกลับ</string> + <string name="action_quick_reply">ตอบกลับด่วน</string> + <string name="report_comment_hint">ความคิดเห็นเพิ่มเติม</string> + <string name="report_username_format">รายงาน @%1$s</string> + <string name="notification_follow_request_format">%1$s ต้องการติดตามคุณ</string> + <string name="notification_follow_format">%1$s ได้ติดตามคุณ</string> + <string name="notification_favourite_format">%1$s ได้ชื่นชอบโพสต์ของคุณ</string> + <string name="notification_reblog_format">%1$s ได้ดันโพสต์ของคุณ</string> + <string name="footer_empty">ไม่มีอะไรที่นี่ ลากลงเพื่อรีเฟรช!</string> + <string name="message_empty">ไม่มีอะไรที่นี่</string> + <string name="post_content_show_less">ย่อ</string> + <string name="post_content_show_more">ขยาย</string> + <string name="post_content_warning_show_less">แสดงน้อยลง</string> + <string name="post_content_warning_show_more">แสดงเพิ่มเติม</string> + <string name="post_sensitive_media_directions">แตะเพื่อดู</string> + <string name="post_media_hidden_title">ซ่อนสื่ออยู่</string> + <string name="post_sensitive_media_title">เนื้อหาอ่อนไหว</string> + <string name="post_boosted_format">%1$s ได้ดัน</string> + <string name="post_username_format">\@%1$s</string> + <string name="title_licenses">สัญญาอนุญาต</string> + <string name="title_scheduled_posts">โพสต์แบบกำหนดเวลา</string> + <string name="title_edit_profile">แก้ไขโปรไฟล์</string> + <string name="title_follow_requests">คำขอติดตาม</string> + <string name="title_domain_mutes">โดเมนที่ซ่อนไว้</string> + <string name="title_blocks">ผู้ใช้ที่ถูกบล็อก</string> + <string name="title_mutes">ผู้ใช้ที่ปิดเสียงไว้</string> + <string name="title_bookmarks">ที่คั่นหน้า</string> + <string name="title_followers">ผู้ติดตาม</string> + <string name="title_follows">ติดตาม</string> + <string name="title_posts_pinned">ปักหมุด</string> + <string name="title_posts_with_replies">โพสต์และตอบกลับ</string> + <string name="title_posts">โพสต์</string> + <string name="title_view_thread">โพสต์</string> + <string name="title_tab_preferences">แท็บ</string> + <string name="title_direct_messages">ข้อความโดยตรง</string> + <string name="title_public_federated">ที่ติดต่อกับภายนอก</string> + <string name="title_public_local">ในเซิร์ฟเวอร์</string> + <string name="title_notifications">การแจ้งเตือน</string> + <string name="title_home">หน้าหลัก</string> + <string name="error_sender_account_gone">การส่งโพสต์เกิดความผิดพลาด</string> + <string name="error_media_upload_sending">อัปโหลดล้มเหลว</string> + <string name="error_media_upload_image_or_video">ไม่สามารถแนบรูปภาพและวิดีโอในโพสต์เดียวกันได้</string> + <string name="error_media_download_permission">ต้องมีสิทธิ์จัดเก็บสื่อ</string> + <string name="error_media_upload_permission">ต้องมีสิทธิ์อ่านสื่อ</string> + <string name="error_media_upload_opening">ไม่สามารถเปิดไฟล์ได้</string> + <string name="error_media_upload_type">ไม่สามารถอัปโหลดไฟล์ประเภทนี้ได้</string> + <string name="error_compose_character_limit">ข้อความสถานะยาวเกินไป!</string> + <string name="error_retrieving_oauth_token">ไม่สามารถรับโทเค็นการเข้าสู่ระบบ</string> + <string name="error_authorization_denied">การขออนุญาตสิทธิถูกปฏิเสธ</string> + <string name="error_authorization_unknown">เกิดข้อผิดพลาดในการขออนุญาตสิทธิโดยไม่ทราบสาเหตุ</string> + <string name="error_no_web_browser_found">ไม่พบเว็บเบราว์เซอร์ที่จะใช้งาน</string> + <string name="error_invalid_domain">โดเมนที่ป้อนไม่ถูกต้อง</string> + <string name="error_empty">ต้องใส่ข้อความ</string> + <string name="error_network">เกิดข้อผิดพลาดเครือข่าย! กรุณาตรวจสอบการเชื่อมต่อและลองอีกครั้ง!</string> + <string name="error_generic">เกิดข้อผิดพลาด</string> + <string name="title_lists">รายการ</string> + <string name="action_lists">รายการ</string> + <string name="about_title_activity">เกี่ยวกับแอปนี้</string> + <string name="action_reset_schedule">ล้างค่า</string> + <string name="action_search">ค้นหา</string> + <string name="action_edit_profile">แก้ไขโปรไฟล์</string> + <string name="action_view_account_preferences">การกำหนดลักษณะบัญชี</string> + <string name="action_view_preferences">การกำหนดลักษณะ</string> + <string name="action_logout">ออกจากระบบ</string> + <string name="title_drafts">ฉบับร่าง</string> + <string name="title_favourites">ชื่นชอบ</string> + <string name="error_failed_app_registration">การยืนยันตัวตนกับเซิร์ฟเวอร์นั้นล้มเหลว</string> + <string name="link_whats_an_instance">Instance คือ\?</string> + <string name="action_login">เข้าสู่ระบบด้วย Mastodon</string> + <string name="poll_allow_multiple_choices">เลือกได้หลายตัวเลือก</string> + <string name="pref_title_confirm_reblogs">แสดงข้อความยืนยันก่อนที่จะบูสต์</string> + <string name="pref_title_show_cards_in_timelines">แสดงตัวอย่างลิงก์ในไทม์ไลน์</string> + <string name="warning_scheduling_interval">Mastodon กำหนดเวลาขั้นต่ำ 5 นาที</string> + <string name="no_scheduled_posts">ไม่มีสถานะแบบตั้งเวลาใด ๆ</string> + <string name="no_drafts">ไม่มีฉบับร่างใด ๆ</string> + <string name="post_lookup_error_format">การค้นหาโพสต์ %1$s เกิดข้อผิดผลาด</string> + <string name="edit_poll">แก้ไข</string> + <string name="poll_new_choice_hint">ตัวเลือกที่ %1$d</string> + <string name="pref_main_nav_position_option_bottom">ล่าง</string> + <string name="pref_main_nav_position_option_top">บน</string> + <string name="pref_main_nav_position">ตำแหน่งการนำทางหลัก</string> + <string name="pref_title_gradient_for_media">แสดงการไล่ระดับสีสันสำหรับสื่อที่ถูกซ่อนไว้</string> + <string name="action_unmute_domain">เลิกปิดเสียง %1$s</string> + <string name="action_unmute_desc">เลิกปิดเสียง %1$s</string> + <string name="dialog_mute_hide_notifications">ซ่อนการแจ้งเตือน</string> + <string name="pref_title_hide_top_toolbar">ซ่อนหัวข้อของแถบเครื่องมือด้านบน</string> + <string name="drafts_post_failed_to_send">ล้มเหลวในการส่งโพสต์นี้!</string> + <string name="wellbeing_mode_notice">ข้อมูลบางอย่างที่อาจส่งผลต่อสุขภาพจิตของคุณจะถูกซ่อนไว้ซึ่งรวมถึง: +\n +\n- การแจ้งเตือน ชื่นชอบ/ดัน/ติดตาม +\n- จำนวนการ ชื่นชอบ/ดัน บนโพสต์ +\n- สถิติ ผู้ติดตาม/โพสต์ ในโปรไฟล์ +\n +\n การแจ้งเตือนแบบพุชจะไม่ได้รับผลกระทบ แต่คุณสามารถตรวจสอบการตั้งค่าการแจ้งเตือนได้ด้วยตนเอง</string> + <string name="limit_notifications">แจ้งเตือน Limit timeline</string> + <string name="review_notifications">แจ้งเตือน Review</string> + <string name="pref_title_notification_filter_subscriptions">ใครบางคนที่ฉันได้ติดตาม ได้เผยแพร่โพสต์ใหม่</string> + <string name="wellbeing_hide_stats_profile">ซ่อนสถิติเชิงปริมาณในโปรไฟล์</string> + <string name="wellbeing_hide_stats_posts">ซ่อนสถิติเชิงปริมาณของโพสต์</string> + <string name="pref_title_wellbeing_mode">สุขภาวะ</string> + <string name="account_note_hint">บันทึกส่วนตัวของคุณเกี่ยวกับบัญชีนี้</string> + <string name="notification_subscription_description">แจ้งเตือน เมื่อคนที่คุณติดตาม ได้เผยแพร่โพสต์ใหม่</string> + <string name="drafts_post_reply_removed">โพสต์ที่คุณได้ร่างตอบไว้ ถูกลบแลัว</string> + <string name="draft_deleted">ลบฉบับร่างแล้ว</string> + <string name="drafts_failed_loading_reply">ล้มเหลวในการโหลดข้อมูลตอบกลับ</string> + <string name="dialog_delete_list_warning">คุณต้องการลบลิสต์ %1$s ใช่ไหม\?</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="other">คุณไม่สามารถอัปโหลดไฟล์แนบมากกว่า %1$d ได้</item> + </plurals> + <string name="account_note_saved">บันทึกแล้ว!</string> + <string name="no_announcements">ไม่มีประกาศ</string> + <string name="duration_indefinite">ไม่มีกำหนด</string> + <string name="label_duration">ระยะเวลา</string> + <string name="post_media_attachments">ไฟล์แนบ</string> + <string name="post_media_audio">เสียง</string> + <string name="notification_subscription_name">โพสต์ใหม่</string> + <string name="notification_subscription_format">%1$s เพิ่งโพสต์</string> + <string name="title_announcements">ประกาศ</string> + <string name="action_delete_conversation">ลบการสนทนา</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml new file mode 100644 index 0000000..5f91f00 --- /dev/null +++ b/app/src/main/res/values-tr/strings.xml @@ -0,0 +1,720 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Bir hata oluştu.</string> + <string name="error_network">Bir ağ hatası oluştu. Lütfen bağlantını kontrol et ve tekrar dene.</string> + <string name="error_empty">Bu alan boş bırakılamaz.</string> + <string name="error_invalid_domain">Girilen alan adı geçersiz</string> + <string name="error_failed_app_registration">Bu sunucu da kimlik doğrulama başarısız oldu. Sorun devam ederse menüdeki Tarayıcıyla Giriş Yap seçeneğini deneyiniz.</string> + <string name="error_no_web_browser_found">Kullanılabilir web tarayıcısı bulunamadı.</string> + <string name="error_authorization_unknown">Tanımlanamayan bir yetkilendirme hatası oluştu. Sorun devam ederse menüdeki Tarayıcı ile Giriş Yap seçeneğini deneyiniz.</string> + <string name="error_authorization_denied">Yetkilendirme reddedildi. Doğru hesap bilgilerini girdiğinizden eminseniz menüdeki Tarayıcı ile Giriş Yap seçeneğini deneyiniz.</string> + <string name="error_retrieving_oauth_token">Giriş belirteci alınırken hata oluştu. Sorun devam ederse menüdeki Tarayıcı ile Giriş Yap seçeneğini deneyiniz.</string> + <string name="error_compose_character_limit">Gönderi çok uzun!</string> + <string name="error_media_upload_type">Bu tür bir dosya yüklenemez.</string> + <string name="error_media_upload_opening">Dosya açılamadı.</string> + <string name="error_media_upload_permission">Medya okuma izni gerekli.</string> + <string name="error_media_download_permission">Medya kaydetme izni gerekli.</string> + <string name="error_media_upload_image_or_video">Görüntüler ve videolar aynı gönderi de yayınlanamaz.</string> + <string name="error_media_upload_sending">Yükleme başarısız oldu.</string> + <string name="error_sender_account_gone">Gönderi yayınlanırken hata.</string> + <string name="title_home">Anasayfa</string> + <string name="title_notifications">Bildirimler</string> + <string name="title_public_local">Yerel</string> + <string name="title_public_federated">Federasyon</string> + <string name="title_direct_messages">Doğrudan iletiler</string> + <string name="title_tab_preferences">Sekmeler</string> + <string name="title_view_thread">Konu</string> + <string name="title_posts">Gönderiler</string> + <string name="title_posts_with_replies">Yanıtlar</string> + <string name="title_posts_pinned">Sabitlenen</string> + <string name="title_follows">Takip edilenler</string> + <string name="title_followers">Takipçiler</string> + <string name="title_favourites">Gözdeler</string> + <string name="title_mutes">Sessize alınmış kullanıcılar</string> + <string name="title_blocks">Engellenmiş kullanıcılar</string> + <string name="title_follow_requests">Takip istekleri</string> + <string name="title_edit_profile">Profili düzenle</string> + <string name="title_drafts">Taslaklar</string> + <string name="title_licenses">Lisanslar</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s yeniden paylaştı</string> + <string name="post_sensitive_media_title">Hassas medya</string> + <string name="post_media_hidden_title">Gizlenmiş medya</string> + <string name="post_sensitive_media_directions">Görüntülemek için dokunun</string> + <string name="post_content_warning_show_more">Daha fazlası</string> + <string name="post_content_warning_show_less">Daha az göster</string> + <string name="post_content_show_more">Genişlet</string> + <string name="post_content_show_less">Daralt</string> + <string name="message_empty">Burada hiçbir şey yok.</string> + <string name="footer_empty">Burada henüz hiç birşey yok. Yenilemek için aşağıya çekin!</string> + <string name="notification_reblog_format">%1$s gönderinizi paylaştı</string> + <string name="notification_favourite_format">%1$s gönderinizi gözdelerilerine ekledi</string> + <string name="notification_follow_format">%1$s seni takip etti</string> + <string name="report_username_format">Bildir @%1$s</string> + <string name="report_comment_hint">Daha fazla yorum?</string> + <string name="action_quick_reply">Hızlı yanıt</string> + <string name="action_reply">Yanıtla</string> + <string name="action_reblog">Yeniden Paylaş</string> + <string name="action_favourite">Gözdele</string> + <string name="action_more">Devamı</string> + <string name="action_compose">Oluştur</string> + <string name="action_login">Tusky ile giriş yap</string> + <string name="action_logout">Oturumu kapat</string> + <string name="action_logout_confirm">Bu oturumu sonlandırmak istediğinizden emin misiniz%1$s\? Bu, taslaklar ve tercihler dahil olmak üzere hesabın tüm yerel verilerini silecektir.</string> + <string name="action_follow">Takip et</string> + <string name="action_unfollow">Takibi bırak</string> + <string name="action_block">Engelle</string> + <string name="action_unblock">Engeli kaldır</string> + <string name="action_hide_reblogs">Yeniden gönderiyi gizle</string> + <string name="action_show_reblogs">Yeniden gönderileri göster</string> + <string name="action_report">Bildir</string> + <string name="action_delete">Sil</string> + <string name="action_send">GÖNDER</string> + <string name="action_send_public">GÖNDER!</string> + <string name="action_retry">Tekrar dene</string> + <string name="action_close">Kapat</string> + <string name="action_view_profile">Profil</string> + <string name="action_view_preferences">Tercihler</string> + <string name="action_view_account_preferences">Hesap tercihleri</string> + <string name="action_view_favourites">Gözdeler</string> + <string name="action_view_mutes">Sessize alınmış kullanıcılar</string> + <string name="action_view_blocks">Engellenmiş kullanıcılar</string> + <string name="action_view_follow_requests">Takip istekleri</string> + <string name="action_view_media">Medya</string> + <string name="action_open_in_web">Tarayıcıda aç</string> + <string name="action_add_media">Medya ekle</string> + <string name="action_photo_take">Fotoğraf çek</string> + <string name="action_share">Paylaş</string> + <string name="action_mute">Sessize al</string> + <string name="action_unmute">Sessizden çıkar</string> + <string name="action_mention">Bahset</string> + <string name="action_hide_media">Medyayı gizle</string> + <string name="action_open_drawer">Çekmece aç</string> + <string name="action_save">Kaydet</string> + <string name="action_edit_profile">Profili düzenle</string> + <string name="action_edit_own_profile">Düzenle</string> + <string name="action_undo">Geri al</string> + <string name="action_accept">Kabul et</string> + <string name="action_reject">Reddet</string> + <string name="action_search">Ara</string> + <string name="action_access_drafts">Taslaklar</string> + <string name="action_toggle_visibility">Gönderi görünürlüğü</string> + <string name="action_content_warning">İçerik uyarısı</string> + <string name="action_emoji_keyboard">İfade klavyesi</string> + <string name="action_add_tab">Sekme ekle</string> + <string name="download_image">%1$s İndiriliyor</string> + <string name="action_copy_link">Bağlantıyı kopyala</string> + <string name="action_open_as">Farklı aç %1$s</string> + <string name="action_share_as">Olarak paylaş …</string> + <string name="send_post_link_to">Gönderi bağlantısını paylaş…</string> + <string name="send_post_content_to">Gönderiyi paylaş…</string> + <string name="send_media_to">Medyayı paylaş…</string> + <string name="confirmation_reported">Gönderildi!</string> + <string name="confirmation_unblocked">Kullanıcının engeli kaldırıldı</string> + <string name="confirmation_unmuted">Kullanıcının sesi açıldı</string> + <string name="hint_domain">Hangi sunucu\?</string> + <string name="hint_compose">Neler oluyor?</string> + <string name="hint_content_warning">İçerik uyarısı</string> + <string name="hint_display_name">Görünen ad</string> + <string name="hint_note">Biyografi</string> + <string name="hint_search">Ara…</string> + <string name="search_no_results">Sonuç bulunamadı</string> + <string name="label_quick_reply">Yanıt…</string> + <string name="label_avatar">Profil Görseli</string> + <string name="label_header">Başlık</string> + <string name="link_whats_an_instance">Sunucu nedir\?</string> + <string name="login_connection">Bağlantı kuruluyor…</string> + <string name="dialog_whats_an_instance">Mastodon gibi herhangi bir adresi veya etki alanını buraya girilebilirsiniz. örneğin: https://mastodon.social, https://mastodon.online, https://qoto.org vb. <a href="https://instances.social">daha fazlası!</a> +\n +\nHenüz hesabınız yoksa, katılmak istediğiniz sunucunun adını girebilir ve orada bir hesap oluşturabilirsiniz. +\n +\nSunucu, hesabınızın barındırıldığı tek yerdir, ancak aynı sitedeymişsiniz gibi diğer sunuculardaki kişilerle kolayca iletişim kurabilir ve onları takip edebilirsiniz. +\n +\n Daha fazla bilgiyi <a href="https://joinmastodon.org">joinmastodon.org</a> adresinde bulabilirsiniz. \u0020</string> + <string name="dialog_title_finishing_media_upload">Medya yüklemesi tamamlanıyor</string> + <string name="dialog_message_uploading_media">Yükleniyor…</string> + <string name="dialog_download_image">İndir</string> + <string name="dialog_message_cancel_follow_request">Takip isteğini iptal et\?</string> + <string name="dialog_unfollow_warning">Takibi bırakmak istiyor musunuz\?</string> + <string name="dialog_delete_post_warning">Bu durumu silmek istiyor musunuz\?</string> + <string name="visibility_public">Herkese Açık: Herkese açık ağ akışında göster</string> + <string name="visibility_unlisted">Listelenmemiş: Herkese açık ağ akışında gösterme</string> + <string name="visibility_private">Özel: Sadece takipçiler ve bahsedilenlere açık</string> + <string name="visibility_direct">Doğrudan: Sadece bahsedilen kullanıcılara açık</string> + <string name="pref_title_edit_notification_settings">Bildirimler</string> + <string name="pref_title_notifications_enabled">Anlık bildirimler</string> + <string name="pref_title_notification_alerts">Uyarılar</string> + <string name="pref_title_notification_alert_sound">Sesli bildirim</string> + <string name="pref_title_notification_alert_vibrate">Titreşimli bildirim</string> + <string name="pref_title_notification_alert_light">Işıklı bildirim</string> + <string name="pref_title_notification_filters">Bana bildir:</string> + <string name="pref_title_notification_filter_mentions">bahsedildiğinde</string> + <string name="pref_title_notification_filter_follows">takip edildiğimde</string> + <string name="pref_title_notification_filter_reblogs">gönderilerim yeniden paylaşıldığında</string> + <string name="pref_title_notification_filter_favourites">gönderilerim gözdelere eklendiğinde</string> + <string name="pref_title_appearance_settings">Görünüş</string> + <string name="pref_title_app_theme">Uygulama teması</string> + <string name="pref_title_timelines">Ağ akışı</string> + <string name="app_them_dark">Koyu</string> + <string name="app_theme_light">Açık</string> + <string name="app_theme_black">Siyah</string> + <string name="app_theme_auto">Gün batımında otomatik</string> + <string name="pref_title_browser_settings">Tarayıcı</string> + <string name="pref_title_custom_tabs">Chrome Özel Sekmelerini Kullan</string> + <string name="pref_title_post_filter">Ağ akışı süzgeçleme</string> + <string name="pref_title_post_tabs">Anasayfa zaman akışı</string> + <string name="pref_title_show_boosts">Yeniden paylaşımları göster</string> + <string name="pref_title_show_replies">Yanıtları göster</string> + <string name="pref_title_show_media_preview">Medya önizlemelerini indir</string> + <string name="pref_title_proxy_settings">Vekil</string> + <string name="pref_title_http_proxy_settings">HTTP vekil sunucu</string> + <string name="pref_title_http_proxy_enable">HTTP vekil sunucu etkinleştir</string> + <string name="pref_title_http_proxy_server">HTTP vekil sunucusu</string> + <string name="pref_title_http_proxy_port">HTTP vekil sunucu portu</string> + <string name="pref_default_post_privacy">Varsayılan gönderi gizliliği (sunucu ile eşleşir)</string> + <string name="pref_default_media_sensitivity">Medyayı her zaman hassas olarak işaretle (sunucu ile eşleşir)</string> + <string name="pref_publishing">Yayın</string> + <string name="pref_failed_to_sync">Ayarlar eşitlendirilemedi</string> + <string name="post_privacy_public">Herkese açık</string> + <string name="post_privacy_unlisted">Liste dışı</string> + <string name="post_privacy_followers_only">Sadece takipçiler</string> + <string name="pref_post_text_size">Yayın metin boyutu</string> + <string name="post_text_size_smallest">Çok küçük</string> + <string name="post_text_size_small">Küçük</string> + <string name="post_text_size_medium">Orta</string> + <string name="post_text_size_large">Büyük</string> + <string name="post_text_size_largest">En büyük</string> + <string name="notification_mention_name">Yeni bahsetmeler</string> + <string name="notification_mention_descriptions">Yeni bahsetmeler hakkında bildirim</string> + <string name="notification_follow_name">Yeni takipçiler</string> + <string name="notification_follow_description">Yeni takipçiler hakkında bildirim</string> + <string name="notification_boost_name">Yeniden Paylaşımlar</string> + <string name="notification_boost_description">Yayınların paylaşıldığında bildirimler</string> + <string name="notification_favourite_name">Gözdeler</string> + <string name="notification_favourite_description">Yayınların gözde olarak işaretlendiğinde</string> + <string name="notification_mention_format">%1$s senden bahsetti</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s ve %4$d diğer</string> + <string name="notification_summary_medium">%1$s, %2$s ve %3$s</string> + <string name="notification_summary_small">%1$s ve %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d yeni etkileşim</item> + <item quantity="other">%1$d yeni etkileşimler</item> + </plurals> + <string name="description_account_locked">Kilitli Hesap</string> + <string name="about_title_activity">Hakkında</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tusky özgür ve açık kaynak bir yazılımdır. GNU Genel Kamu Lisansı sürüm 3 altında lisanslanmıştır. Lisansı buradan görüntüleyebilirsiniz: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">Proje web sayfası: https://tusky.app</string> + <string name="about_bug_feature_request_site">Hata raporları ve özellik istekleri: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Tusky\'nin Profili</string> + <string name="post_share_content">Gönderinin içeriğini paylaş</string> + <string name="post_share_link">Gönderinin bağlantısını paylaş</string> + <string name="post_media_images">Görseller</string> + <string name="post_media_video">Video</string> + <string name="state_follow_requested">Takip istenen</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">%1$dy içinde</string> + <string name="abbreviated_in_days">%1$dd içinde</string> + <string name="abbreviated_in_hours">%1$dh içinde</string> + <string name="abbreviated_in_minutes">%1$dm içinde</string> + <string name="abbreviated_in_seconds">%1$ds içinde</string> + <string name="abbreviated_years_ago">%1$dy</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="follows_you">Seni takip ediyor</string> + <string name="pref_title_alway_show_sensitive_media">Her zaman hassas içerikleri göster</string> + <string name="title_media">Medya</string> + <string name="load_more_placeholder_text">daha fazlası</string> + <string name="add_account_name">Hesap Ekle</string> + <string name="add_account_description">Yeni Mastodon hesabı ekle</string> + <string name="action_lists">Listeler</string> + <string name="title_lists">Listeler</string> + <string name="compose_active_account_description">%1$s hesabıyla gönderiliyor</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="one">İçeriği görme engelliler için açıkla (%1$d karakter limiti)</item> + <item quantity="other">İçerikleri görme engelliler için açıklamalar (%1$d karakter limitleri)</item> + </plurals> + <string name="action_set_caption">Başlık belirle</string> + <string name="action_remove">Kaldır</string> + <string name="lock_account_label">Hesabı Kilitle</string> + <string name="lock_account_label_description">Takipçileri elle onaylamanız gerekir</string> + <string name="compose_save_draft">Taslaklara kaydedilsin mi\?</string> + <string name="send_post_notification_title">Gönderi gönderiliyor…</string> + <string name="send_post_notification_error_title">Gönderi yayınlanırken hata oluştu</string> + <string name="send_post_notification_channel_name">Gönderiler Yayınlanıyor</string> + <string name="send_post_notification_cancel_title">Gönderme iptal edildi</string> + <string name="send_post_notification_saved_content">Gönderinin bir kopyası taslaklara kaydedildi</string> + <string name="action_compose_shortcut">Oluştur</string> + <string name="error_no_custom_emojis">%1$s örneğinizin herhangi bir özel ifadesi yok</string> + <string name="emoji_style">İfade stili</string> + <string name="system_default">Sistem varsayılanı</string> + <string name="download_fonts">Önce bu ifade paketini indirmeniz gerekecek</string> + <string name="performing_lookup_title">Araştırılıyor…</string> + <string name="expand_collapse_all_posts">Tüm durumları Genişlet/Küçült</string> + <string name="action_open_post">Gönderiyi aç</string> + <string name="restart_required">Uygulamayı yeniden başlatmanız gerekmekte</string> + <string name="restart_emoji">Bu değişiklikleri uygulamak için Tusky\'yi yeniden başlatmanız gerekecek</string> + <string name="later">Sonra</string> + <string name="restart">Yeniden başlat</string> + <string name="caption_systememoji">Cihazınızın varsayılan ifade paketi</string> + <string name="caption_blobmoji">Android 4.4–7.1\'den bilinen Blob ifadeleri</string> + <string name="caption_twemoji">Mastodon\'un standart ifade paketi</string> + <string name="download_failed">İndirme başarısız</string> + <string name="account_moved_description">%1$s buraya taşındı:</string> + <string name="unreblog_private">Yeniden paylaşımı iptal et</string> + <string name="license_description">Tusky aşağıdakı açık kaynaklı projelerden kod ve materyal içeriyor:</string> + <string name="license_apache_2">Apache Lisansı altında lisanslanmıştır (kopya aşağıda)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">Profil meta verileri</string> + <string name="profile_metadata_add">veri ekle</string> + <string name="profile_metadata_label_label">Etiket</string> + <string name="profile_metadata_content_label">İçerik</string> + <string name="pref_title_absolute_time">Kesin tarih kullan</string> + <string name="label_remote_account">Aşağıdaki bilgiler değişken olabilir. Tüm profili tarayıcıda görmek için tuşlayın.</string> + <string name="unpin_action">Sabitlemeyi kaldır</string> + <string name="pin_action">Sabitle</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> Gözde</item> + <item quantity="other"><b>%1$s</b> Gözdeler</item> + </plurals> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> Paylaşan</item> + <item quantity="other"><b>%1$s</b> Paylaşanlar</item> + </plurals> + <string name="title_reblogged_by">tarafından paylaşıldı</string> + <string name="title_favourited_by">Tarafından gözdelendi</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s ve %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s ve %3$d dahası</string> + <string name="title_domain_mutes">Gizli alan adları</string> + <string name="action_unreblog">Yeniden paylaşmaktan vazgeç</string> + <string name="action_unfavourite">Gözdeyi kaldır</string> + <string name="action_view_domain_mutes">Gizli alan adları</string> + <string name="action_mute_domain">%1$s alan adını sessize al</string> + <string name="action_links">Bağlantılar</string> + <string name="action_hashtags">Etiketler</string> + <string name="action_open_faved_by">Gözdeleri göster</string> + <string name="title_hashtags_dialog">Etiketler</string> + <string name="title_links_dialog">Bağlantılar</string> + <string name="download_media">Medya indir</string> + <string name="downloading_media">Medya indiriliyor</string> + <string name="mute_domain_warning">%1$s\? alan adından gelen her şeyi engellemek istediğinden emin misin\? Bu alan adından gelen içeriği herhangi bir genel zaman tünelinde veya bildirimlerinde göremezsiniz. Bu alan adındaki takipçilerin de kaldırılacak.</string> + <string name="pref_title_notification_filter_poll">anketler sona erdiğinde</string> + <string name="pref_title_timeline_filters">Süzgeçler</string> + <string name="app_theme_system">Sistem tasarımını kullan</string> + <string name="pref_title_language">Dil</string> + <string name="pref_title_animate_gif_avatars">Hareketli GIF görsellerini oynat</string> + <string name="notification_poll_name">Anketler</string> + <string name="notification_poll_description">Sona eren anketlerle ilgili bildirimler</string> + <string name="pref_title_public_filter_keywords">Genel ağ akışı</string> + <string name="pref_title_thread_filter_keywords">Konuşmalar</string> + <string name="filter_addition_title">Süzgeç ekle</string> + <string name="filter_edit_title">Süzgeci düzenle</string> + <string name="filter_dialog_remove_button">Kaldır</string> + <string name="filter_dialog_update_button">Güncelle</string> + <string name="filter_dialog_whole_word">Tüm dünya</string> + <string name="filter_dialog_whole_word_description">Bir anahtar kelime veya kelime öbeği sadece alfanümerik olduğunda, yalnızca tüm kelimeyle eşleşirse uygulanır</string> + <string name="filter_add_description">Süzgeçlenecek ifade</string> + <string name="error_create_list">Liste oluşturulamadı</string> + <string name="error_rename_list">Liste oluşturulamadı</string> + <string name="error_delete_list">Liste silinemedi</string> + <string name="action_create_list">Liste oluştur</string> + <string name="action_rename_list">Listeyi güncelle</string> + <string name="action_delete_list">Listeyi sil</string> + <string name="hint_search_people_list">Takip ettiğim kişilerde ara</string> + <string name="action_add_to_list">Listeye hesap ekle</string> + <string name="action_remove_from_list">Hesabı listeden kaldır</string> + <string name="caption_notoemoji">Google\'ın mevcut ifade paketi</string> + <string name="description_post_media">Ortam: %1$s</string> + <string name="description_post_cw">İçerik uyarısı: %1$s</string> + <string name="description_post_media_no_description_placeholder">Açıklama yok</string> + <string name="description_post_reblogged">Yeniden blogladı</string> + <string name="description_post_favourited">Gözdelendi</string> + <string name="description_visibility_public">Herkese açık</string> + <string name="description_visibility_unlisted">Liste dışı</string> + <string name="description_visibility_private">Takipçiler</string> + <string name="description_visibility_direct">Doğrudan</string> + <string name="description_poll">Seçenekli anket: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="hint_list_name">Liste adı</string> + <string name="edit_hashtag_hint"># Etiket olmadan</string> + <string name="notifications_clear">Sil</string> + <string name="notifications_apply_filter">Süzgeçle</string> + <string name="filter_apply">Uygula</string> + <string name="compose_shortcut_long_label">Yayın Oluştur</string> + <string name="compose_shortcut_short_label">Oluştur</string> + <string name="notification_clear_text">Tüm bildirimleri kalıcı olarak silmek istediğinizden emin misiniz\?</string> + <string name="compose_preview_image_description">%1$s görüntüsü için eylemler</string> + <string name="poll_info_format"> \u0020<!-- 15 oy • 1 saat kaldı --> \u0020%1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s oy</item> + <item quantity="other">%1$s oylar</item> + </plurals> + <string name="poll_info_closed">kapandı</string> + <string name="poll_vote">Oy</string> + <string name="poll_ended_voted">Oy verdiğin bir anket sona erdi</string> + <string name="poll_ended_created">Oluşturduğun bir anket sona erdi</string> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d gün kaldı</item> + <item quantity="other">%1$d günler kaldı</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d saat kaldı</item> + <item quantity="other">%1$d saatler kaldı</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d dakika kaldı</item> + <item quantity="other">%1$d dakikalar kaldı</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d saniye kaldı</item> + <item quantity="other">%1$d saniyeler kaldı</item> + </plurals> + <string name="button_continue">Devam</string> + <string name="button_back">Geri</string> + <string name="button_done">Tamam</string> + <string name="report_sent_success">\@%1$s başarıyla bildirildi</string> + <string name="hint_additional_info">Ek Yorumlar</string> + <string name="report_remote_instance">%1$s adresine ilet</string> + <string name="failed_fetch_posts">Yayınlar getirilemedi</string> + <string name="report_description_1">Bildirim sunucu yöneticinize gönderilecektir. Bu hesabı neden raporladığınızla ilgili açıklama yapabilirsiniz:</string> + <string name="report_description_remote_instance">Hesap başka bir sunucudan. Raporun anonim bir kopyasını da oraya gönderilsin mi\?</string> + <string name="action_mentions">Bahsedenler</string> + <string name="action_open_reblogger">Gönderi yazanını aç</string> + <string name="action_open_reblogged_by">Yeniden paylaşımları göster</string> + <string name="title_mentions_dialog">Bahsedenler</string> + <string name="action_open_media_n">#%1$d medyayı aç</string> + <string name="title_bookmarks">Yerimleri</string> + <string name="title_scheduled_posts">Zamanlanmış yayınlar</string> + <string name="action_bookmark">Yerimi</string> + <string name="action_edit">Düzenle</string> + <string name="action_delete_and_redraft">Sil ve düzenle</string> + <string name="action_view_bookmarks">Yerimleri</string> + <string name="action_add_poll">Anket ekle</string> + <string name="action_access_scheduled_posts">Zamanlanmış yayınlar</string> + <string name="action_schedule_post">Gönderi tarihini ayarla</string> + <string name="action_reset_schedule">Sıfırla</string> + <string name="dialog_redraft_post_warning">Bu durumu silip yeniden düzenlemek istiyor musunuz\?</string> + <string name="pref_title_bot_overlay">Botlar için işaret göster</string> + <string name="about_powered_by_tusky">Tusky tarafından desteklenmektedir</string> + <string name="description_post_bookmarked">Yerimine eklendi</string> + <string name="select_list_title">Liste seç</string> + <string name="list">Liste</string> + <string name="title_accounts">Hesaplar</string> + <string name="failed_search">Arama başarısız</string> + <string name="create_poll_title">Anket</string> + <string name="duration_5_min">5 dakika</string> + <string name="duration_30_min">30 dakika</string> + <string name="duration_1_hour">1 saat</string> + <string name="duration_6_hours">6 saat</string> + <string name="duration_1_day">1 gün</string> + <string name="duration_3_days">3 gün</string> + <string name="duration_7_days">7 gün</string> + <string name="add_poll_choice">Seçenek ekle</string> + <string name="poll_allow_multiple_choices">Çoklu seçim</string> + <string name="edit_poll">Düzenle</string> + <string name="replying_to">Yanıtlanıyor: %1$s</string> + <string name="profile_badge_bot_text">Alt Metin</string> + <string name="confirmation_domain_unmuted">%1$s alan adını gizleme</string> + <string name="mute_domain_warning_dialog_ok">Alan adından her şeyi gizle</string> + <string name="pref_title_alway_open_spoiler">Her zaman içerik uyarılarıyla işaretlenmiş alanları genişlet</string> + <string name="poll_info_time_absolute">%1$s içinde sona erecek</string> + <string name="failed_report">Raporlanamadı</string> + <string name="poll_new_choice_hint">Seçenek %1$d</string> + <string name="post_lookup_error_format">%1$s gönderisi aranırken hata oluştu</string> + <string name="no_drafts">Hiç taslağın yok.</string> + <string name="no_scheduled_posts">Zamanlanmış yayınınız yok.</string> + <string name="reblog_private">Kendi kitlenizle yeniden paylaşın</string> + <string name="hashtags">Etiketler</string> + <string name="pref_title_confirm_reblogs">Yeniden paylaşmadan önce onay kutusunu göster</string> + <string name="pref_title_show_cards_in_timelines">Bağlantı önizlemelerini zaman tünelinde göster</string> + <string name="pref_title_enable_swipe_for_tabs">Sekmeler arasında geçiş yapmak için kaydırma hareketini etkinleştir</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s kişi</item> + <item quantity="other">%1$s kişiler</item> + </plurals> + <string name="add_hashtag_title">Etiket ekle</string> + <string name="notification_follow_request_description">Takip istekleri ile ilgili bildirimler</string> + <string name="notification_follow_request_name">Takip istekleri</string> + <string name="pref_main_nav_position_option_bottom">Alt</string> + <string name="pref_main_nav_position_option_top">Üst</string> + <string name="dialog_mute_hide_notifications">Bildirimleri gizle</string> + <string name="dialog_mute_warning">\@%1$s sessize al\?</string> + <string name="dialog_block_warning">\@%1$s engellensin mi\?</string> + <string name="action_unmute_domain">%1$s alan adının sesini aç</string> + <string name="action_unmute_desc">%1$s sesini aç</string> + <string name="notification_follow_request_format">%1$s seni takip etmek istiyor</string> + <string name="action_unmute_conversation">Sohbetin sesini aç</string> + <string name="pref_title_notification_filter_follow_requests">takip isteklerinde</string> + <string name="action_mute_conversation">Sohbeti sessize al</string> + <string name="pref_main_nav_position">Ana gezinti konumu</string> + <string name="pref_title_gradient_for_media">Gizli medya için renkli gradyanlar göster</string> + <string name="warning_scheduling_interval">Mastodon\'un minimum 5 dakikalık zamanlama aralığı vardır.</string> + <string name="pref_title_hide_top_toolbar">Üst araç çubuğunun başlığını gizle</string> + <string name="action_delete_conversation">Konuşmayı sil</string> + <string name="title_announcements">Duyurular</string> + <string name="error_multimedia_size_limit">Video ve ses dosyaları %1$s MB boyutunu aşamaz.</string> + <string name="error_image_edit_failed">Görsel düzenlenemedi.</string> + <string name="error_following_hashtag_format">#%1$s\'i izleyen hata</string> + <string name="title_login">Giriş</string> + <string name="error_loading_account_details">Hesap detayları alınırken hata</string> + <string name="error_could_not_load_login_page">Giriş ekranı yüklenemedi.</string> + <string name="error_muting_hashtag_format">Sessize alma hatası #%1$s</string> + <string name="action_unfollow_hashtag_format">Takibi bırak #%1$s\?</string> + <string name="pref_title_wellbeing_mode">Nicelikselrefah</string> + <string name="account_note_saved">Kaydedildi!</string> + <string name="report_category_spam">İstenmeyen</string> + <string name="report_category_other">Diğer</string> + <string name="pref_show_self_username_always">Daima</string> + <string name="pref_show_self_username_disambiguate">Birden fazla oturum açıldığında</string> + <string name="pref_show_self_username_never">Asla</string> + <string name="pref_title_confirm_favourites">Gözdelere eklemeden önce onay kutusunu göster</string> + <string name="error_unmuting_hashtag_format">Susturmayı kaldırma hatası #%1$s</string> + <string name="pref_title_notification_filter_reports">yeni bir rapor olduğunda</string> + <string name="pref_title_animate_custom_emojis">Özel ayarlanmış emojileri oynat</string> + <string name="notification_header_report_format">%1$s rapor edilmiş %2$s</string> + <string name="notification_summary_report_format">%1$s · %2$d ekli gönderiler</string> + <string name="post_edited">Düzenleme %1$s</string> + <string name="account_note_hint">Bu hesap hakkındaki özel notun</string> + <string name="title_migration_relogin">Bildirim beslemesi için yeniden giriş yapın</string> + <string name="title_followed_hashtags">Takip edilen etiketler</string> + <string name="notification_sign_up_format">%1$s kaydol</string> + <string name="action_unbookmark">Yerimini kaldır</string> + <string name="drafts_failed_loading_reply">Yanıt bilgisi yüklenemedi</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">adetten fazla %1$d medya eki yükleyemezsiniz.</item> + <item quantity="other">adetten fazla %1$d medya ekleri yükleyemezsiniz.</item> + </plurals> + <string name="delete_scheduled_post_warning">Bu zamanlanmış yayını sil\?</string> + <string name="pref_title_notification_filter_updates">etkileşimde bulunduğun bir gönderi düzenledi</string> + <string name="instance_rule_info">Giriş yaparak aşağıdaki kuralları kabul etmiş sayılırsınız %1$s.</string> + <string name="instance_rule_title">%1$s kural</string> + <string name="set_focus_description">Küçük görsellerde her zaman görülebilecek odak noktasını seçmek için daireye dokunun veya sürükleyin.</string> + <string name="action_add_or_remove_from_list">Listeden ekle veya kaldır</string> + <string name="failed_to_add_to_list">Hesap listeye eklenemedi</string> + <string name="failed_to_remove_from_list">Hesap listeden kaldırılamadı</string> + <string name="no_announcements">Hiç duyuru yok.</string> + <string name="no_lists">Listen yok.</string> + <string name="duration_30_days">30 gün</string> + <string name="compose_save_draft_loses_media">Taslağı kurtarmak mı\? (Taslağı geri yüklediğinizde ekler yeniden yüklenir.)</string> + <string name="pref_title_show_self_username">Kullanıcı adını araç çubuklarında göster</string> + <string name="duration_180_days">180 gün</string> + <string name="duration_60_days">60 gün</string> + <string name="duration_90_days">90 gün</string> + <string name="pref_default_post_language">Varsayılan gönderme dili (sunucu ile eşleşir)</string> + <string name="notification_report_name">Raporlar</string> + <string name="notification_report_description">Denetleme raporlarıyla ilgili bildirimler</string> + <string name="duration_no_change">(değişiklik yok)</string> + <string name="duration_14_days">14 gün</string> + <string name="duration_365_days">365 gün</string> + <string name="review_notifications">Bildirimleri Gözden Geçir</string> + <string name="wellbeing_mode_notice">İyi kullanım için bazı bilgiler gizlenebilir.. Bunlar: +\n +\n - Gözde/Yeniden Paylaşım/Takip bildirimleri +\n - Gözde/Yeniden Paylaşım sayacı +\n - Takipçi/Gönderi istatistikleri +\n +\nBesleme bildirimleri etkilenmeyecek , ama manuel olarak tercihlerinizi gözden geçirebilirsiniz.</string> + <string name="dialog_delete_list_warning">%1$s listesini gerçekten silmek istiyor musunuz\?</string> + <string name="wellbeing_hide_stats_profile">Profillerdeki niceliksel istatistikleri gizle</string> + <string name="tusky_compose_post_quicksetting_label">Gönderi Oluştur</string> + <string name="saving_draft">Taslağı kaydediyor…</string> + <string name="action_set_focus">Odak noktasını ayarla</string> + <string name="action_edit_image">Görseli düzenle</string> + <string name="account_date_joined">%1$s katıldı</string> + <string name="hint_media_description_missing">Medyanın bir açıklaması olmalı.</string> + <string name="dialog_delete_conversation_warning">Bu görüşmeyi sil\?</string> + <string name="pref_title_notification_filter_subscriptions">abone olduğum birisi yeni bir gönderi yayınladı</string> + <string name="pref_title_notification_filter_sign_ups">birisi kaydolmuş</string> + <string name="post_media_audio">Ses</string> + <string name="post_media_attachments">Ekler</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="failed_to_pin">Sabitleme Başarısız</string> + <string name="failed_to_unpin">Açılamadı</string> + <string name="drafts_post_failed_to_send">Bu yayın gönderilemedi!</string> + <string name="limit_notifications">Ağ akışı bildirimlerini sınırla</string> + <string name="wellbeing_hide_stats_posts">Yayınların niceliksel istatistikleri gizle</string> + <string name="status_created_at_now">şimdi</string> + <string name="drafts_post_reply_removed">Hazırladığınız yanıtınız yayından kaldırıldı</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="report_category_violation">Kural ihlali</string> + <string name="description_post_language">Gönderi dili</string> + <string name="dialog_push_notification_migration">Birleşik itme aracılığıyla, itme bildirimlerini kullanmak için Tusky\'nin Mastodon sunucundaki bildirimlere abone olma iznine ihtiyacı var. Bu, Tusky\'ye verilen OAuth kapsamlarını değiştirmek için yeniden oturum açmayı gerektirir. Burada veya Hesap tercihleri bölümünde yeniden giriş yapma seçeneğini kullanman tüm yerel taslaklarını ve önbelleğini koruyacaktır.</string> + <string name="confirmation_hashtag_unfollowed">#%1$s takip edilmeyenler</string> + <string name="action_subscribe_account">Abone Ol</string> + <string name="follow_requests_info">Hesabınız kilitli olmasa da, %1$s kadro bu hesaplardan gelen takip isteklerini elle gözden geçirmek isteyebileceğinizi düşündü.</string> + <string name="notification_report_format">Hakkında yeni rapor %1$s</string> + <string name="action_add_reaction">tepki ekle</string> + <string name="action_dismiss">Yoksay</string> + <string name="action_details">Ayrıntılar</string> + <string name="notification_subscription_name">Yeni yayınlar</string> + <string name="notification_subscription_description">abone olduğunuz birisi yeni gönderi yayınladığında gelen bildirimler</string> + <string name="notification_sign_up_name">Kayıt Ol</string> + <string name="notification_sign_up_description">Yeni kullanıcılar hakkında bildirimler</string> + <string name="notification_update_name">Düzenlemeleri yayınla</string> + <string name="notification_update_description">Etkileşimde bulunduğunuz gönderiler düzenlendiğinde bildirimler</string> + <string name="duration_indefinite">Tanımsız</string> + <string name="description_post_edited">Düzenlendi</string> + <string name="action_unsubscribe_account">Abonelikten Çık</string> + <string name="dialog_push_notification_migration_other_accounts">Tusky\'ye bildirim aboneliği izni vermek için mevcut hesabınıza yeniden giriş yaptınız. Ancak, yine de bu şekilde geçirilmemiş başka hesaplarınız var. UnifiedPush bildirimleri desteğini etkinleştirmek için bunlara geçin ve tek tek yeniden giriş yapın.</string> + <string name="error_following_hashtags_unsupported">Bu sunucu aşağıdaki etiketleri desteklemez.</string> + <string name="notification_subscription_format">%1$s az önce paylaşım yaptı</string> + <string name="notification_update_format">%1$s gönderilerini düzenledi</string> + <string name="draft_deleted">Taslak Silindi</string> + <string name="error_unfollowing_hashtag_format">Takip etmeyi bırakırken hata #%1$s</string> + <string name="status_count_one_plus">1+</string> + <string name="label_duration">Süre</string> + <string name="tips_push_notification_migration">Bildirim besleme desteğini etkinleştirmek için tüm hesaplara yeniden giriş yapın.</string> + <string name="error_status_source_load">Sunucudan kaynak durumu yüklenemedi.</string> + <string name="pref_title_http_proxy_port_message">Bağlantı noktası %1$d ile %2$d arasında olmalıdır</string> + <string name="post_media_alt">Alternatif metin</string> + <string name="a11y_label_loading_thread">Konu yükleniyor</string> + <string name="help_empty_home">Bu senin <b>ana ağ akışın</b>. Takip ettiğin hesapların son gönderileri burada yer alacak. +\n +\nTakip edebileceğin hesapları diğer ağ akışlarından keşfedebilirsin, örneğin kendi sunucunun yerel zaman tünelinden [iconics gmd_group]. Veya hesapları adlarıyla arayabilirsin [iconics gmd_search], örneğin Mastodon hesabımızı bulmak için Tusky adıyla araman yeterli.</string> + <string name="pref_title_reading_order">Okuma sırası</string> + <string name="pref_reading_order_oldest_first">Önce en eski</string> + <string name="pref_reading_order_newest_first">Önce en yeni</string> + <string name="pref_summary_http_proxy_missing"><ayarlanmadı></string> + <string name="pref_summary_http_proxy_invalid"><geçersiz></string> + <string name="pref_summary_http_proxy_disabled">Etkisizleştirildi</string> + <string name="status_created_info">%1$s oluşturdu</string> + <string name="dialog_follow_hashtag_title">Etiketi takip et</string> + <string name="dialog_follow_hashtag_hint">#etiket</string> + <string name="compose_unsaved_changes">Kaydedilmemiş değişikliklerin var.</string> + <string name="status_edit_info">%1$s düzenledi</string> + <string name="action_refresh">Yenile</string> + <string name="title_edits">Düzenlemeler</string> + <string name="post_media_image">Görsel</string> + <string name="label_filter_action">Süzgeçleme eylemi</string> + <string name="action_add">Ekle</string> + <string name="label_filter_context">Süzgeçleme bağlamları</string> + <string name="filter_keyword_addition_title">Anahtar kelime ekle</string> + <string name="label_filter_keywords">Süzgeçlenecek anahtar kelimeler veya kelime öbekleri</string> + <string name="filter_keyword_display_format">%1$s (tüm dünya)</string> + <string name="action_post_failed">Yükleme başarısız</string> + <string name="action_post_failed_detail">Gönderin yüklenemedi ve taslaklara kaydedildi. +\n +\nSunucuya bağlanılamamış veya sunucu gönderiyi reddetmiş olabilir.</string> + <string name="action_post_failed_detail_plural">Gönderilerin yüklenemedi ve taslaklara kaydedildi. +\n +\nSunucuya bağlanılamamış veya sunucu gönderiyi reddetmiş olabilir.</string> + <string name="action_post_failed_show_drafts">Taslakları göster</string> + <string name="action_post_failed_do_nothing">Yoksay</string> + <string name="notification_unknown_name">Bilinmeyen</string> + <string name="socket_timeout_exception">Sunucuyla bağlantı kurmak çok uzun sürdü</string> + <string name="ui_error_unknown">bilinmeyen sebep</string> + <string name="ui_error_bookmark">Yerimi gönderisi başarısız oldu: %1$s</string> + <string name="ui_error_vote">Ankete oy gönderilemedi: %1$s</string> + <string name="ui_error_clear_notifications">Bildirimler temizlenemedi: %1$s</string> + <string name="ui_error_favourite">Gönderi gözdelere eklenemedi: %1$s</string> + <string name="ui_error_accept_follow_request">Takip isteği kabul edilemedi: %1$s</string> + <string name="ui_error_reject_follow_request">Takip isteği reddedilemedi: %1$s</string> + <string name="ui_error_reblog">Gönderi yeniden paylaşılamadı: %1$s</string> + <string name="ui_success_accepted_follow_request">Takip isteği kabul edildi</string> + <string name="ui_success_rejected_follow_request">Takip isteği engellendi</string> + <string name="action_browser_login">Tarayıcı ile Giriş Yap</string> + <string name="description_login">Çoğu durumda çalışır. Diğer uygulamalara veri sızmaz.</string> + <string name="description_browser_login">Ek kimlik doğrulama yöntemlerini destekleyebilir ancak desteklenen bir tarayıcı gerektirir.</string> + <string name="select_list_manage">Listeleri yönet</string> + <string name="action_share_account_link">Hesap bağlantısını paylaş</string> + <string name="action_share_account_username">Hesap adını paylaş</string> + <string name="account_username_copied">Kullanıcı adı kopyalandı</string> + <string name="send_account_username_to">Hesabın kullanıcı adını şununla paylaş…</string> + <string name="send_account_link_to">Hesabın bağlantısını şununla paylaş…</string> + <string name="mute_notifications_switch">Bildirimleri sessize al</string> + <string name="action_discard">Değişiklikleri yoksay</string> + <string name="action_continue_edit">Düzenlemeye devam et</string> + <string name="pref_title_account_filter_keywords">Profiller</string> + <string name="filter_description_hide">Tamamen gizle</string> + <string name="hint_filter_title">Süzgecim</string> + <string name="label_filter_title">Başlık</string> + <string name="filter_action_warn">Uyarı</string> + <string name="filter_action_hide">Gizle</string> + <string name="filter_description_warn">Bir uyarı ile gizle</string> + <string name="status_filtered_show_anyway">Yine de göster</string> + <string name="status_filter_placeholder_label_format">Süzgeçlendi: %1$s</string> + <string name="filter_edit_keyword_title">Anahtar kelimeyi düzenle</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="pref_title_show_stat_inline">Yayın istatistiklerini sağ akışında göster</string> + <string name="title_public_trending_hashtags">Öne çıkan etiketler</string> + <string name="total_usage">Toplam kullanım</string> + <string name="total_accounts">Toplam hesap</string> + <string name="accessibility_talking_about_tag">%1$d kişi %2$s etiketi hakkında konuşuyor</string> + <string name="pref_ui_text_size">UI yazı boyutu</string> + <string name="notification_listenable_worker_name">Arkaplan etkinliği</string> + <string name="notification_listenable_worker_description">Tusky arka planda çalışırken gelen bildirimler</string> + <string name="notification_notification_worker">Bildirimler getiriliyor…</string> + <string name="notification_prune_cache">Önbellek bakımı…</string> + <string name="load_newest_notifications">En yeni bildirimleri yükle</string> + <string name="compose_delete_draft">Taslağı sil\?</string> + <string name="error_missing_edits">Sunucunuz bu gönderinin düzenlendiğini bilir, ancak düzenlemelerin bir kopyası yoktur, bu nedenle bunlar size gösterilemez. +\n +\nBu <a href="https://github.com/mastodon/mastodon/issues/25398">Mastodon sorununu #25398</a>.</string> + <string name="error_media_upload_sending_fmt">Yükleme başarısız oldu: %1$s</string> + <string name="about_device_info_title">Cihazınız</string> + <string name="about_device_info">%1$s %2$s +\nAndroid sürüm: %3$s +\nSDK sürüm: %4$d</string> + <string name="about_account_info_title">Hesabınız</string> + <string name="about_account_info">\@%1$s@%2$s +\nSürüm: %3$s</string> + <string name="about_copy">Sürüm ve cihaz bilgilerini kopyala</string> + <string name="about_copied">Kopyalanan sürüm ve cihaz bilgileri</string> + <string name="list_exclusive_label">Anasayfa ağ akışından gizle</string> + <string name="error_media_playback">Oynatma başarısız oldu: %1$s</string> + <string name="error_blocking_domain">Sessize alınamadı %1$s: %2$s</string> + <string name="dialog_delete_filter_text">Süzgeci sil \'%1$s\'\?</string> + <string name="dialog_delete_filter_positive_action">Sil</string> + <string name="dialog_save_profile_changes_message">Profil değişikliklerinizi kaydetmek istiyor musunuz\?</string> + <string name="help_empty_conversations">İşte <b>özel iletileriniz</b>; bazen konuşmalar veya doğrudan iletiler (Dİ) olarak da adlandırılır. +\n +\nÖzel iletiler, bir gönderinin [iconics gmd_public] görünürlüğünü [iconics gmd_mail] <i>Doğrudan</i> olarak ayarlayarak ve metinde bir veya daha fazla kullanıcıdan bahsederek oluşturulur. +\n +\nÖrneğin, bir hesabın profil görünümünde başlayabilir ve oluştur düğmesine [iconics gmd_edit] dokunabilir ve görünürlüğü değiştirebilirsiniz. </string> + <string name="help_empty_lists">Bu sizin <b>liste görünümünüzdür</b>. Bir dizi özel liste tanımlayabilir ve bunlara hesaplar ekleyebilirsiniz. +\n +\n Yalnızca takip ettiğiniz hesapları listelerinize ekleyebileceğinizi unutmayın. +\n +\n \u0020Bu listeler Hesap tercihleri [iconics gmd_account_circle] [iconics gmd_navigate_next] Sekmelerinde sekme olarak kullanılabilir. </string> + <string name="muting_hashtag_success_format">Uyarı olarak #%1$s etiketini susturma</string> + <string name="unmuting_hashtag_success_format">Etiket sesi kapatılıyor #%1$s</string> + <string name="action_view_filter">Süzgeci görüntüle</string> + <string name="following_hashtag_success_format">Şimdi etiketi takip edin #%1$s</string> + <string name="unfollowing_hashtag_success_format">Artık etiketi takip etmiyorum #%1$s</string> + <string name="error_unblocking_domain">Sesi açılamadı %1$s: %2$s</string> + <string name="title_public_trending_statuses">Gündemdekiler</string> + <string name="label_image">Görsel</string> + <string name="app_theme_system_black">Sistem Tasarımını Kullanın (siyah)</string> + <string name="list_reply_policy_list">Liste üyeleri</string> + <string name="list_reply_policy_followed">Takip edilen herhangi bir kullanıcı</string> + <string name="list_reply_policy_label">Yanıtları göster</string> + <string name="list_reply_policy_none">Hiç kimse</string> + <string name="pref_title_show_self_boosts">Kendi beğenmeleri göster</string> + <string name="pref_title_show_self_boosts_description">Birisi kendi gönderisini yükseltiyor</string> + <string name="pref_title_per_timeline_preferences">Zaman akışı başına tercihler</string> + <string name="pref_title_show_notifications_filter">Bildirim süzgecini göster</string> + <string name="reply_sending">Gönderiliyor…</string> + <string name="reply_sending_long">Cevabınız gönderiliyor.</string> + <string name="action_show_original">Özgünü göster</string> + <string name="label_translated">%1$s dilinden %2$s ile çevrildi</string> + <string name="ui_error_translate">Çeviri yapılamadı: %1$s</string> + <string name="action_translate">Çevir</string> + <string name="label_translating">Çeviri…</string> + <string name="report_category_legal">Yasal</string> + <string name="unknown_notification_type">Bilinmeyen bildirim türü</string> + <string name="dialog_follow_warning">Bu hesabı mı takip ediyorsun?</string> + <string name="pref_title_confirm_follows">Takip etmeden önce onayı göster</string> + <string name="url_copied">Url kopyalandı</string> + <string name="confirmation_hashtag_copied">\'#%1$s\' kopyalandı</string> + <string name="pref_default_reply_privacy">Varsayılan yanıt gizliliği (sunucu ile eşleştirilmedi)</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml new file mode 100644 index 0000000..940dbf9 --- /dev/null +++ b/app/src/main/res/values-uk/strings.xml @@ -0,0 +1,734 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">Сталася помилка.</string> + <string name="post_username_format">\@%1$s</string> + <string name="title_licenses">Ліцензії</string> + <string name="title_edit_profile">Редагувати профіль</string> + <string name="title_follow_requests">Запити на стеження</string> + <string name="title_bookmarks">Закладки</string> + <string name="title_followers">Підписники</string> + <string name="title_follows">Підписки</string> + <string name="title_posts_pinned">Прикріплені</string> + <string name="title_posts_with_replies">З відповідями</string> + <string name="title_public_federated">Загальні</string> + <string name="title_public_local">Локальні</string> + <string name="title_direct_messages">Приватні повідомлення</string> + <string name="title_notifications">Сповіщення</string> + <string name="title_home">Головна</string> + <string name="error_sender_account_gone">Помилка надсилання допису.</string> + <string name="error_media_upload_image_or_video">Зображення та відео не можуть бути прикріплені до допису одночасно.</string> + <string name="error_media_download_permission">Потрібен дозвіл на зберігання медіа.</string> + <string name="error_media_upload_permission">Потрібен дозвіл на читання медіа.</string> + <string name="error_media_upload_opening">Не вдається відкрити цей файл.</string> + <string name="error_media_upload_type">Неможливо відвантажити файл цього типу.</string> + <string name="error_compose_character_limit">Допис задовгий!</string> + <string name="error_no_web_browser_found">Не вдалося знайти браузер, який можна використати.</string> + <string name="error_empty">Не може бути порожнім.</string> + <string name="error_network">Сталася помилка мережі. Перевірте інтернет-з\'єднання та спробуйте знову.</string> + <string name="title_lists">Списки</string> + <string name="action_lists">Списки</string> + <string name="about_title_activity">Про застосунок</string> + <string name="action_reset_schedule">Скинути</string> + <string name="action_search">Пошук</string> + <string name="action_edit_profile">Редагувати профіль</string> + <string name="action_view_account_preferences">Налаштування облікового запису</string> + <string name="action_view_preferences">Налаштування</string> + <string name="action_logout">Вийти</string> + <string name="title_drafts">Чернетки</string> + <string name="title_favourites">Вподобане</string> + <string name="action_login">Увійти з Tusky</string> + <string name="login_connection">Зʼєднання…</string> + <string name="search_no_results">Немає результатів</string> + <string name="hint_search">Пошук…</string> + <string name="hint_note">Про себе</string> + <string name="hint_compose">Що відбувається\?</string> + <string name="confirmation_reported">Надіслано!</string> + <string name="action_share_as">Поділитися як …</string> + <string name="action_open_as">Відкрити як %1$s</string> + <string name="action_copy_link">Копіювати посилання</string> + <string name="download_image">Завантаження %1$s</string> + <string name="title_links_dialog">Посилання</string> + <string name="title_mentions_dialog">Згадки</string> + <string name="action_open_faved_by">Показати, хто вподобав</string> + <string name="action_mentions">Згадки</string> + <string name="action_links">Посилання</string> + <string name="action_content_warning">Попередження про вміст</string> + <string name="action_access_scheduled_posts">Заплановані дописи</string> + <string name="action_access_drafts">Чернетки</string> + <string name="action_reject">Відхилити</string> + <string name="action_accept">Прийняти</string> + <string name="action_undo">Скасувати</string> + <string name="action_edit_own_profile">Змінити</string> + <string name="action_save">Зберегти</string> + <string name="action_mention">Згадати</string> + <string name="action_mute_domain">Сховати %1$s</string> + <string name="action_mute">Сховати</string> + <string name="action_share">Поділитися</string> + <string name="action_photo_take">Сфотографувати</string> + <string name="action_add_poll">Додати опитування</string> + <string name="action_add_media">Додати медіа</string> + <string name="action_open_in_web">Відкрити в браузері</string> + <string name="action_view_media">Медіа</string> + <string name="action_view_follow_requests">Запити на стеження</string> + <string name="action_view_blocks">Заблоковані користувачі</string> + <string name="action_view_bookmarks">Закладки</string> + <string name="action_view_favourites">Вподобане</string> + <string name="action_view_profile">Профіль</string> + <string name="action_close">Закрити</string> + <string name="action_retry">Повторити</string> + <string name="action_delete_and_redraft">Видалити та перестворити</string> + <string name="action_delete">Видалити</string> + <string name="action_edit">Змінити</string> + <string name="action_report">Поскаржитися</string> + <string name="action_unblock">Розблокувати</string> + <string name="action_block">Заблокувати</string> + <string name="action_unfollow">Не стежити</string> + <string name="action_follow">Підписатися</string> + <string name="action_logout_confirm">Ви впевнені, що хочете вийти %1$s\? Це призведе до видалення всіх локальних даних облікового запису, включно з чернетками та вподобаннями.</string> + <string name="action_compose">Написати</string> + <string name="action_unfavourite">Не подобається</string> + <string name="action_bookmark">Додати в закладки</string> + <string name="action_favourite">Вподобати</string> + <string name="action_reply">Відповісти</string> + <string name="action_quick_reply">Швидка відповідь</string> + <string name="report_comment_hint">Додаткові коментарі\?</string> + <string name="report_username_format">Поскаржитися на @%1$s</string> + <string name="notification_follow_request_format">%1$s надсилає запит на підписку</string> + <string name="notification_follow_format">%1$s підписується на вас</string> + <string name="footer_empty">Тут нічого немає. Потягніть вниз, щоб оновити!</string> + <string name="message_empty">Нічого немає.</string> + <string name="post_content_warning_show_less">Згорнути</string> + <string name="post_content_warning_show_more">Розгорнути</string> + <string name="post_sensitive_media_directions">Натисніть для перегляду</string> + <string name="post_media_hidden_title">Медіа приховано</string> + <string name="hint_content_warning">Попередження про вміст</string> + <string name="edit_poll">Змінити</string> + <string name="compose_shortcut_short_label">Написати</string> + <string name="action_unmute_conversation">Не ховати розмову</string> + <string name="action_mute_conversation">Сховати розмову</string> + <string name="title_scheduled_posts">Заплановані дописи</string> + <string name="description_visibility_private">Підписники</string> + <string name="action_compose_shortcut">Написати</string> + <string name="title_media">Медіа</string> + <string name="pref_title_notifications_enabled">Сповіщення</string> + <string name="pref_title_edit_notification_settings">Сповіщення</string> + <string name="title_blocks">Заблоковані користувачі</string> + <string name="notification_favourite_name">Вподобане</string> + <string name="notification_follow_request_name">Запити на стеження</string> + <string name="action_unmute_desc">Не ховати %1$s</string> + <string name="action_unmute">Не ховати</string> + <string name="action_view_domain_mutes">Сховані домени</string> + <string name="action_view_mutes">Приховувані користувачі</string> + <string name="action_send_public">ПОШИРИТИ!</string> + <string name="action_send">ПОШИРИТИ</string> + <string name="action_show_reblogs">Показати поширення</string> + <string name="action_hide_reblogs">Сховати поширення</string> + <string name="action_more">Розгорнути</string> + <string name="action_unreblog">Скасувати поширення</string> + <string name="action_reblog">Поширити</string> + <string name="notification_favourite_format">%1$s вподобує ваш допис</string> + <string name="notification_reblog_format">%1$s поширює ваш допис</string> + <string name="post_content_show_less">Згорнути</string> + <string name="post_content_show_more">Розгорнути</string> + <string name="post_sensitive_media_title">Делікатний вміст</string> + <string name="post_boosted_format">%1$s поширює</string> + <string name="title_domain_mutes">Сховані домени</string> + <string name="title_mutes">Приховувані користувачі</string> + <string name="title_posts">Дописи</string> + <string name="title_view_thread">Тред</string> + <string name="title_tab_preferences">Вкладки</string> + <string name="error_media_upload_sending">Не вдалося відвантажити.</string> + <string name="error_retrieving_oauth_token">Не вдалося отримати токен входу. Якщо проблема не зникає, спробуйте увійти через браузер з меню.</string> + <string name="error_authorization_denied">Авторизацію відхилено. Якщо ви впевнені, що вказали правильні облікові дані, спробуйте увійти через браузер з меню.</string> + <string name="error_authorization_unknown">Сталася помилка невпізнання авторизації. Якщо проблема не зникає, спробуйте увійти через браузер з меню.</string> + <string name="error_failed_app_registration">Помилка автентифікації цього сервера. Якщо проблема не зникає, спробуйте увійти через браузер з меню.</string> + <string name="error_invalid_domain">Введено недійсний домен</string> + <string name="action_open_reblogged_by">Показати поширення</string> + <string name="pref_title_show_boosts">Показати поширення</string> + <string name="pref_title_post_tabs">Головна стрічка</string> + <string name="action_unmute_domain">Не ховати %1$s</string> + <string name="action_toggle_visibility">Видимість дописів</string> + <string name="wellbeing_mode_notice">Деякі відомості, які можуть вплинути на ваше психічний стан, буде приховано. Це включає: +\n +\n - Уподобання/Поширення/Сповіщення про підписки +\n - Уподобання/Кількість поширень дописів +\n - Статистика підписників/поширень у профілях +\n +\n На push-сповіщення це не вплине, але ви можете переглянути налаштування сповіщень вручну.</string> + <string name="description_post_favourited">Уподобано</string> + <string name="title_favourited_by">Вподобали</string> + <plurals name="favs"> + <item quantity="one"><b>%1$s</b> вподобання</item> + <item quantity="few"><b>%1$s</b> вподобання</item> + <item quantity="many"><b>%1$s</b> вподобань</item> + <item quantity="other"><b>%1$s</b> вподобань</item> + </plurals> + <string name="notification_favourite_description">Сповіщати про вподобання кимось дописів</string> + <string name="pref_title_notification_filter_favourites">мої дописи вподобано</string> + <string name="action_hide_media">Сховати медіа</string> + <string name="notification_subscription_format">%1$s щойно опубліковано</string> + <string name="title_announcements">Оголошення</string> + <string name="action_open_drawer">Відкрити меню</string> + <string name="caption_blobmoji">Емодзі Blob з Android 4.4–7.1</string> + <string name="caption_systememoji">Типовий набір емодзі пристрою</string> + <string name="performing_lookup_title">Виконання пошуку…</string> + <string name="download_fonts">Спочатку потрібно буде завантажити ці набори емодзі</string> + <string name="system_default">Типовий системний</string> + <string name="emoji_style">Стиль емодзі</string> + <string name="error_no_custom_emojis">Ваш сервер %1$s не має власних емодзі</string> + <string name="compose_save_draft">Зберегти чернетку\?</string> + <string name="lock_account_label_description">Вимагає затвердження підписників власноруч</string> + <string name="action_set_caption">Додати підпис</string> + <plurals name="hint_describe_for_visually_impaired"> + <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="action_unsubscribe_account">Відписатися</string> + <string name="action_subscribe_account">Підписатися</string> + <string name="account_note_saved">Збережено!</string> + <string name="poll_new_choice_hint">Вибір %1$d</string> + <string name="poll_allow_multiple_choices">Кілька виборів</string> + <string name="add_poll_choice">Додати вибір</string> + <string name="duration_7_days">7 днів</string> + <string name="duration_3_days">3 дні</string> + <string name="duration_1_day">1 день</string> + <string name="duration_6_hours">6 годин</string> + <string name="duration_1_hour">1 година</string> + <string name="duration_30_min">30 хвилин</string> + <string name="duration_5_min">5 хвилин</string> + <string name="duration_indefinite">Безкінечно</string> + <string name="label_duration">Тривалість</string> + <string name="create_poll_title">Опитування</string> + <string name="title_accounts">Облікові записи</string> + <string name="failed_report">Не вдалося звітувати</string> + <string name="hint_additional_info">Додаткові коментарі</string> + <string name="report_sent_success">Звіт @%1$s надіслано</string> + <string name="button_done">Готово</string> + <string name="button_back">Назад</string> + <string name="button_continue">Продовжити</string> + <plurals name="poll_timespan_seconds"> + <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> + <plurals name="poll_timespan_minutes"> + <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> + <plurals name="poll_timespan_hours"> + <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> + <plurals name="poll_timespan_days"> + <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="poll_ended_created">Створене вами опитування завершилося</string> + <string name="poll_ended_voted">Опитування, в якому ви проголосували</string> + <string name="poll_vote">Голосувати</string> + <string name="poll_info_closed">закрито</string> + <string name="poll_info_time_absolute">завершується о %1$s</string> + <plurals name="poll_info_people"> + <item quantity="one">%1$s особа</item> + <item quantity="few">%1$s людини</item> + <item quantity="many">%1$s людей</item> + <item quantity="other">%1$s людей</item> + </plurals> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s голос</item> + <item quantity="few">%1$s голоси</item> + <item quantity="many">%1$s голосів</item> + <item quantity="other">%1$s голосів</item> + </plurals> + <string name="poll_info_format"> \u0020<!-- 15 голосів • 1 година залишилась --> \u0020%1$s • %2$s</string> + <string name="description_post_media_no_description_placeholder">Без опису</string> + <string name="description_post_cw">Попередження про матеріали: %1$s</string> + <string name="description_post_media">Медіа: %1$s</string> + <string name="title_reblogged_by">Поширює</string> + <string name="profile_metadata_content_label">Вміст</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="profile_badge_bot_text">Бот</string> + <string name="restart">Перезапустити</string> + <string name="later">Пізніше</string> + <string name="restart_emoji">Вам потрібно буде перезапустити Tusky, щоб застосувати ці зміни</string> + <string name="restart_required">Необхідно перезапустити застосунок</string> + <string name="action_open_post">Відкрити допис</string> + <string name="expand_collapse_all_posts">Розгорнути/згорнути всі дописи</string> + <string name="send_post_notification_saved_content">Копію допису збережено до ваших чернеток</string> + <string name="send_post_notification_cancel_title">Надсилання скасовано</string> + <string name="send_post_notification_channel_name">Надсилання дописів</string> + <string name="send_post_notification_error_title">Помилка надсилання допису</string> + <string name="send_post_notification_title">Надсилання допису…</string> + <string name="compose_active_account_description">Публікування як %1$s</string> + <string name="action_remove_from_list">Вилучити обліковий запис зі списку</string> + <string name="action_add_to_list">Додати обліковий запис до списку</string> + <string name="hint_search_people_list">Пошук серед тих, на кого ви підписані</string> + <string name="action_delete_list">Видалити список</string> + <string name="action_rename_list">Оновити список</string> + <string name="action_create_list">Створити список</string> + <string name="error_delete_list">Не вдалося видалити список</string> + <string name="error_rename_list">Не вдалося оновити список</string> + <string name="error_create_list">Не вдалося створити список</string> + <string name="add_account_description">Додати новий обліковий запис Mastodon</string> + <string name="add_account_name">Додати обліковий запис</string> + <string name="filter_add_description">Фільтрувати фразу</string> + <string name="filter_dialog_whole_word_description">Коли ключове слово або фраза є лише буквено-цифровими, вони застосовуватимуться лише, якщо вони збігатимуться з цілим словом</string> + <string name="filter_dialog_whole_word">Ціле слово</string> + <string name="filter_dialog_update_button">Оновити</string> + <string name="lock_account_label">Заблокувати обліковий запис</string> + <string name="action_remove">Вилучити</string> + <string name="filter_dialog_remove_button">Вилучити</string> + <string name="filter_edit_title">Редагувати фільтр</string> + <string name="filter_addition_title">Додати фільтр</string> + <string name="pref_title_thread_filter_keywords">Розмови</string> + <string name="pref_title_public_filter_keywords">Загальнодоступні стрічки</string> + <string name="load_more_placeholder_text">завантажити ще</string> + <string name="replying_to">Відповідь для @%1$s</string> + <string name="pref_title_alway_open_spoiler">Завжди розгортати допис, з попередженнями про вміст</string> + <string name="follows_you">Підписники</string> + <string name="pref_title_alway_show_sensitive_media">Завжди показувати делікатний вміст</string> + <string name="abbreviated_seconds_ago">%1$dс</string> + <string name="abbreviated_minutes_ago">%1$dхв</string> + <string name="abbreviated_hours_ago">%1$dгод</string> + <string name="abbreviated_days_ago">%1$dдн</string> + <string name="abbreviated_years_ago">%1$dр.</string> + <string name="abbreviated_in_seconds">за %1$dс</string> + <string name="abbreviated_in_minutes">за %1$dхв</string> + <string name="abbreviated_in_hours">за %1$dгод</string> + <string name="abbreviated_in_days">за %1$dдн</string> + <string name="abbreviated_in_years">за %1$dр.</string> + <string name="state_follow_requested">Запит на підписку надіслано</string> + <string name="post_media_attachments">Вкладення</string> + <string name="post_media_audio">Звуки</string> + <string name="post_media_video">Відео</string> + <string name="post_media_images">Зображення</string> + <string name="post_share_link">Поділитися посиланням на допис</string> + <string name="post_share_content">Поділитися вмістом допису</string> + <string name="about_tusky_account">Профіль Tusky</string> + <string name="about_bug_feature_request_site">Звіти про вади та запити функцій: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_project_site">Вебсайт проєкту: https://tusky.app</string> + <string name="about_tusky_license">Tusky — вільне та відкрите програмне забезпечення. Ліцензовано загальною громадською ліцензією GNU версії 3, ви можете переглянути ліцензію тут: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="about_powered_by_tusky">Створено Tusky</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="description_account_locked">Заблокований обліковий запис</string> + <plurals name="notification_title_summary"> + <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="conversation_more_recipients">%1$s, %2$s та ще %3$d</string> + <string name="conversation_2_recipients">%1$s та %2$s</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="notification_summary_small">%1$s та %2$s</string> + <string name="notification_summary_medium">%1$s, %2$s, та %3$s</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s та %4$d інших</string> + <string name="notification_mention_format">%1$s згадує вас</string> + <string name="notification_subscription_description">Сповіщати про нові дописи осіб, на яких ви підписалися</string> + <string name="notification_subscription_name">Нові дописи</string> + <string name="notification_poll_description">Сповіщати про завершення опитувань</string> + <string name="notification_poll_name">Опитування</string> + <string name="notification_boost_description">Сповіщати про поширення ваших дописів</string> + <string name="notification_boost_name">Поширення</string> + <string name="notification_follow_request_description">Сповіщати про нові запити на підписки</string> + <string name="notification_follow_description">Сповіщати про нових підписників</string> + <string name="notification_follow_name">Нові підписники</string> + <string name="notification_mention_descriptions">Сповіщати про нові згадки</string> + <string name="notification_mention_name">Нові згадки</string> + <string name="post_text_size_largest">Найбільший</string> + <string name="post_text_size_large">Великий</string> + <string name="post_text_size_medium">Середній</string> + <string name="post_text_size_small">Маленький</string> + <string name="post_text_size_smallest">Найменший</string> + <string name="pref_post_text_size">Розмір шрифту допису</string> + <string name="post_privacy_followers_only">Лише для підписників</string> + <string name="description_visibility_unlisted">Приховано</string> + <string name="post_privacy_unlisted">Приховано</string> + <string name="description_visibility_public">Публічно</string> + <string name="post_privacy_public">Публічно</string> + <string name="pref_main_nav_position_option_bottom">Внизу</string> + <string name="pref_main_nav_position_option_top">Вгорі</string> + <string name="pref_main_nav_position">Розташування головної панелі переходів</string> + <string name="pref_failed_to_sync">Не вдалося синхронізувати налаштування</string> + <string name="pref_publishing">Публікування (синхронізовано з сервером)</string> + <string name="pref_default_media_sensitivity">Завжди позначати дописи делікатними</string> + <string name="pref_default_post_privacy">Типова приватність дописів</string> + <string name="pref_title_http_proxy_port">Порт HTTP-проксі</string> + <string name="pref_title_http_proxy_server">Сервер HTTP-проксі</string> + <string name="pref_title_http_proxy_enable">Увімкнути HTTP-проксі</string> + <string name="pref_title_http_proxy_settings">HTTP-проксі</string> + <string name="pref_title_proxy_settings">Проксі</string> + <string name="pref_title_show_media_preview">Завантаження попереднього перегляду медіа</string> + <string name="pref_title_show_replies">Показати відповіді</string> + <string name="pref_title_post_filter">Фільтрування стрічки</string> + <string name="pref_title_animate_custom_emojis">Анімувати власні емодзі</string> + <string name="pref_title_gradient_for_media">Показувати барвисті градієнти замість прихованих медіа</string> + <string name="pref_title_animate_gif_avatars">Анімовані GIF-аватарки</string> + <string name="pref_title_bot_overlay">Показувати позначки для ботів</string> + <string name="pref_title_language">Мова</string> + <string name="pref_title_custom_tabs">Вкладки вбудованого браузера Chrome</string> + <string name="pref_title_browser_settings">Браузер</string> + <string name="app_theme_system">Тема системи</string> + <string name="app_theme_auto">Автоматична від заходу сонця</string> + <string name="app_theme_black">Чорна</string> + <string name="app_theme_light">Світла</string> + <string name="app_them_dark">Темна</string> + <string name="pref_title_timeline_filters">Фільтри</string> + <string name="pref_title_timelines">Стрічки</string> + <string name="pref_title_app_theme">Тема застосунку</string> + <string name="pref_title_appearance_settings">Вигляд</string> + <string name="pref_title_notification_filter_subscriptions">хтось, на кого мене підписано, публікує новий допис</string> + <string name="pref_title_notification_filter_poll">опитування завершено</string> + <string name="pref_title_notification_filter_reblogs">мої дописи поширено</string> + <string name="pref_title_notification_filter_follow_requests">отримано запит на підписку</string> + <string name="pref_title_notification_filter_follows">хтось підписується</string> + <string name="pref_title_notification_filter_mentions">мене згадано</string> + <string name="pref_title_notification_filters">Сповіщати мене коли</string> + <string name="pref_title_notification_alert_light">Світлосповіщення</string> + <string name="pref_title_notification_alert_vibrate">Вібросповіщення</string> + <string name="pref_title_notification_alert_sound">Звукові сповіщення</string> + <string name="pref_title_notification_alerts">Попередження</string> + <string name="visibility_direct">Безпосередньо: Опублікувати лише для згаданих користувачів</string> + <string name="visibility_private">Лише підписники: Опублікувати лише для підписників</string> + <string name="visibility_unlisted">Приховано: Не показувати у загальних стрічках</string> + <string name="visibility_public">Публічно: Опублікувати у загальних стрічках</string> + <string name="dialog_mute_hide_notifications">Сховати сповіщення</string> + <string name="dialog_mute_warning">Сховати @%1$s?</string> + <string name="dialog_block_warning">Заблокувати @%1$s?</string> + <string name="mute_domain_warning_dialog_ok">Сховати весь домен</string> + <string name="mute_domain_warning">Ви впевнені, що хочете заблокувати все з %1$s? Ви не побачите матеріали з цього домену в загальнодоступних стрічках або у своїх сповіщеннях. Ваших підписників з цього домену буде вилучено.</string> + <string name="dialog_redraft_post_warning">Видалити й переписати цей допис\?</string> + <string name="dialog_delete_post_warning">Видалити цей допис\?</string> + <string name="dialog_unfollow_warning">Не стежити за цим обліковим записом\?</string> + <string name="dialog_message_cancel_follow_request">Відкликати запит на підписку\?</string> + <string name="dialog_download_image">Завантаження</string> + <string name="dialog_message_uploading_media">Відвантаження…</string> + <string name="dialog_title_finishing_media_upload">Завершення відвантаження медіа</string> + <string name="dialog_whats_an_instance">Сюди можна ввести адресу або домен будь-якого сервера, наприклад mastodon.social, icosahedron.website, social.tchncs.de та <a href="https://instances.social">більше!</a> \u0020 +\n \u0020 +\nЯкщо у вас ще немає облікового запису, ви можете ввести назву сервера, до якого ви хочете приєднатися та створити там обліковий запис. \u0020 +\n \u0020 +\nСервер — єдине місце, де розміщено ваш обліковий запис, але ви можете легко спілкуватися з людьми та стежити за ними на інших серверах, ніби ви перебуваєте на тому ж сайті. \u0020 +\n \u0020 +\nДокладніше на <a href="https://joinmastodon.org">joinmastodon.org</a>. до них облікові записи. \u0020</string> + <string name="link_whats_an_instance">Що таке сервер\?</string> + <string name="hint_domain">Котрий сервер\?</string> + <string name="label_header">Заголовок</string> + <string name="label_avatar">Аватар</string> + <string name="label_quick_reply">Відповісти…</string> + <string name="hint_display_name">Показуване ім\'я</string> + <string name="confirmation_domain_unmuted">%1$s показано</string> + <string name="confirmation_unmuted">Приховування користувача скасовано</string> + <string name="confirmation_unblocked">Користувача розблоковано</string> + <string name="send_media_to">Поділитися медіа з…</string> + <string name="send_post_content_to">Поділитися дописом з…</string> + <string name="send_post_link_to">Поділитися URL-адресою допису з…</string> + <string name="downloading_media">Завантаження медіа</string> + <string name="download_media">Завантажити медіа</string> + <string name="action_open_media_n">Відкрити медіа #%1$d</string> + <string name="hashtags">Хештеги</string> + <string name="action_hashtags">Хештеги</string> + <string name="title_hashtags_dialog">Хештеги</string> + <string name="action_open_reblogger">Відкрити автора поширення</string> + <string name="action_add_tab">Додати вкладку</string> + <string name="action_schedule_post">Запланувати допис</string> + <string name="action_emoji_keyboard">Клавіотура емодзі</string> + <string name="drafts_post_reply_removed">Допис, для якого ви створили чернетку відповіді, вилучено</string> + <string name="draft_deleted">Чернетку видалено</string> + <string name="drafts_failed_loading_reply">Не вдалося завантажити дані відповіді</string> + <string name="drafts_post_failed_to_send">Не вдалося надіслати цей допис!</string> + <string name="dialog_delete_list_warning">Ви дійсно хочете видалити список %1$s?</string> + <plurals name="error_upload_max_media_reached"> + <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="wellbeing_hide_stats_profile">Приховати кількісну статистику профілів</string> + <string name="wellbeing_hide_stats_posts">Приховати кількісну статистику дописів</string> + <string name="limit_notifications">Обмеження сповіщень стрічки</string> + <string name="review_notifications">Переглянути сповіщення</string> + <string name="account_note_hint">Ваша особиста примітка про цей обліковий запис</string> + <string name="pref_title_wellbeing_mode">Добробут</string> + <string name="pref_title_hide_top_toolbar">Сховати заголовок верхньої панелі інструментів</string> + <string name="pref_title_confirm_reblogs">Запитувати підтвердження перед поширенням</string> + <string name="pref_title_show_cards_in_timelines">Показувати попередній перегляд посилань у стрічках</string> + <string name="warning_scheduling_interval">Найкоротший час планування Mastodon становить 5 хвилин.</string> + <string name="no_announcements">Оголошень немає.</string> + <string name="no_scheduled_posts">Немає запланованих дописів.</string> + <string name="no_drafts">У вас немає чернеток.</string> + <string name="post_lookup_error_format">Помилка пошуку допису %1$s</string> + <string name="pref_title_enable_swipe_for_tabs">Увімкнути перемикання між вкладками жестом проведення пальцем</string> + <string name="failed_search">Не вдалося здійснити пошук</string> + <string name="report_description_remote_instance">Обліковий запис з іншого сервера. Надіслати анонімізовану копію звіту й туди\?</string> + <string name="report_description_1">Скаргу буде надіслано вашому модератору сервера. Ви можете надати пояснення, чому ви повідомляєте про цей обліковий запис знизу:</string> + <string name="failed_fetch_posts">Не вдалося отримати дописи</string> + <string name="report_remote_instance">Переслати до %1$s</string> + <string name="compose_preview_image_description">Дії для зображення %1$s</string> + <string name="notification_clear_text">Ви впевнені, що хочете остаточно очистити всі сповіщення\?</string> + <string name="compose_shortcut_long_label">Створити допис</string> + <string name="filter_apply">Застосувати</string> + <string name="notifications_apply_filter">Фільтр</string> + <string name="notifications_clear">Видалити</string> + <string name="list">Список</string> + <string name="select_list_title">Вибрати список</string> + <string name="edit_hashtag_hint">Хештег без #</string> + <string name="add_hashtag_title">Додати хештег</string> + <string name="hint_list_name">Назва списку</string> + <string name="description_poll">Опитування з варіантами: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="description_visibility_direct">Безпосередньо</string> + <string name="description_post_bookmarked">Додано до закладок</string> + <string name="description_post_reblogged">Поширено</string> + <plurals name="reblogs"> + <item quantity="one"><b>%1$s</b> поширення</item> + <item quantity="few"><b>%1$s</b> поширення</item> + <item quantity="many"><b>%1$s</b> поширень</item> + <item quantity="other"><b>%1$s</b> поширень</item> + </plurals> + <string name="pin_action">Прикріпити</string> + <string name="unpin_action">Відкріпити</string> + <string name="label_remote_account">Наведені далі відомості можуть відбивати не повний профіль користувача. Натисніть, щоб відкрити повний профіль у браузері.</string> + <string name="pref_title_absolute_time">Показ абсолютного часу</string> + <string name="profile_metadata_label_label">Ярлик</string> + <string name="profile_metadata_add">додати дані</string> + <string name="profile_metadata_label">Метадані профілю</string> + <string name="license_apache_2">Ліцензовано ліцензією Apache (копія знизу)</string> + <string name="license_description">Tusky містить код та засоби з таких проєктів з відкритим кодом:</string> + <string name="unreblog_private">Скасувати поширення</string> + <string name="reblog_private">Поширити початковій аудиторії</string> + <string name="account_moved_description">%1$s переміщено до:</string> + <string name="download_failed">Не вдалося завантажити</string> + <string name="caption_notoemoji">Поточний набір емодзі Google</string> + <string name="caption_twemoji">Стандартний набір емодзі Mastodon</string> + <string name="follow_requests_info">Навіть попри те, що ваш обліковий запис загальнодоступний, співробітники %1$s вважають, що ви, можливо, захочете переглянути запити від цих облікових записів власноруч.</string> + <string name="dialog_delete_conversation_warning">Видалити цю бесіду\?</string> + <string name="action_delete_conversation">Видалити бесіду</string> + <string name="action_unbookmark">Вилучити закладку</string> + <string name="pref_title_confirm_favourites">Запитувати підтвердження перед додаванням до вподобаних</string> + <string name="duration_14_days">14 днів</string> + <string name="duration_30_days">30 днів</string> + <string name="duration_60_days">60 днів</string> + <string name="duration_90_days">90 днів</string> + <string name="duration_180_days">180 днів</string> + <string name="duration_365_days">365 днів</string> + <string name="tusky_compose_post_quicksetting_label">Створити допис</string> + <string name="notification_sign_up_format">%1$s реєструється</string> + <string name="pref_title_notification_filter_sign_ups">хтось реєструється</string> + <string name="notification_sign_up_name">Реєстрації</string> + <string name="notification_sign_up_description">Сповіщення про нових користувачів</string> + <string name="notification_update_format">%1$s редагує свій допис</string> + <string name="pref_title_notification_filter_updates">допис, з яким у мене була взаємодія, відредаговано</string> + <string name="notification_update_description">Сповіщення, коли редагується повідомлення, з яким ви взаємодіяли</string> + <string name="notification_update_name">Редакції допису</string> + <string name="title_login">Вхід</string> + <string name="error_could_not_load_login_page">Не вдалося завантажити сторінку входу.</string> + <string name="saving_draft">Збереження чернетки…</string> + <string name="action_dismiss">Відхилити</string> + <string name="action_details">Подробиці</string> + <string name="title_migration_relogin">Увійдіть повторно, щоб отримувати push-сповіщення</string> + <string name="tips_push_notification_migration">Увійдіть повторно до всіх облікових записів, щоб увімкнути підтримку push-сповіщень.</string> + <string name="dialog_push_notification_migration">Щоб використовувати push-сповіщення через UnifiedPush, Tusky потребує дозволу стежити за сповіщеннями на вашому сервері Mastodon. Це вимагає повторного входу, щоб змінити області OAuth, надані Tusky. Використання параметра повторного входу тут або в налаштуваннях облікового запису збереже всі ваші локальні чернетки та кеш.</string> + <string name="dialog_push_notification_migration_other_accounts">Ви повторно увійшли до свого поточного облікового запису, щоб надати дозвіл на стеження Tusky. Однак у вас все ще є інші облікові записи, які не мігрували таким чином. Перейдіть до них і повторно увійдіть до них по одному, щоб забезпечити підтримку UnifiedPush сповіщень.</string> + <string name="account_date_joined">Приєднується %1$s</string> + <string name="action_edit_image">Редагувати зображення</string> + <string name="status_count_one_plus">1+</string> + <string name="error_image_edit_failed">Неможливо редагувати зображення.</string> + <string name="error_loading_account_details">Не вдалося завантажити подробиці облікового запису</string> + <string name="error_following_hashtag_format">Помилка підписки на #%1$s</string> + <string name="error_multimedia_size_limit">Розмір відео та аудіофайлів не може перевищувати %1$s Мб.</string> + <string name="error_unfollowing_hashtag_format">Помилка скасування підписки на #%1$s</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="description_post_language">Мова допису</string> + <string name="duration_no_change">(Не змінено)</string> + <string name="action_set_focus">Налаштувати точку фокусування</string> + <string name="pref_show_self_username_always">Завжди</string> + <string name="set_focus_description">Торкніться або перетягніть коло, щоб вибрати точку фокусування, яку завжди буде видно на мініатюрах.</string> + <string name="pref_show_self_username_disambiguate">Якщо ви ввійшли у кілька облікових записів</string> + <string name="pref_show_self_username_never">Ніколи</string> + <string name="pref_title_show_self_username">Показувати ім\'я користувача на панелях інструментів</string> + <string name="delete_scheduled_post_warning">Видалити цей запланований допис\?</string> + <string name="instance_rule_info">Увійшовши, ви погоджуєтесь з правилами %1$s.</string> + <string name="instance_rule_title">Правила %1$s</string> + <string name="failed_to_pin">Не вдалося прикріпити</string> + <string name="failed_to_unpin">Не вдалося відкріпити</string> + <string name="compose_save_draft_loses_media">Зберегти чернетку\? (Вкладення будуть завантажені знову, коли ви відновите чернетку.)</string> + <string name="action_add_reaction">додати реакцію</string> + <string name="notification_report_format">Нова скарга %1$s</string> + <string name="report_category_violation">Порушення правил</string> + <string name="action_add_or_remove_from_list">Додати або вилучити зі списку</string> + <string name="failed_to_add_to_list">Не вдалося додати обліковий запис до списку</string> + <string name="failed_to_remove_from_list">Не вдалося вилучити обліковий запис зі списку</string> + <string name="action_unfollow_hashtag_format">Не слідкувати за #%1$s?</string> + <string name="status_created_at_now">зараз</string> + <string name="notification_header_report_format">%1$s скаржиться %2$s</string> + <string name="notification_summary_report_format">%1$s · %2$d прикріплених дописів</string> + <string name="confirmation_hashtag_unfollowed">#%1$s не відстежується</string> + <string name="pref_title_notification_filter_reports">з\'явилася нова скарга</string> + <string name="notification_report_name">Скарги</string> + <string name="notification_report_description">Сповіщення про звіти про модерацію</string> + <string name="no_lists">У вас немає списків.</string> + <string name="report_category_spam">Спам</string> + <string name="report_category_other">Інше</string> + <string name="error_following_hashtags_unsupported">Цей сервер не підтримує слідкування за хештегами.</string> + <string name="title_followed_hashtags">Відстежувані хештеги</string> + <string name="pref_default_post_language">Типова мова дописів</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="post_edited">Змінено %1$s</string> + <string name="description_post_edited">Змінено</string> + <string name="error_muting_hashtag_format">Помилка приховування #%1$s</string> + <string name="error_unmuting_hashtag_format">Помилка увімкнення сповіщень #%1$s</string> + <string name="hint_media_description_missing">Медіа повинні мати опис.</string> + <string name="pref_title_http_proxy_port_message">Порт повинен бути між %1$d і %2$d</string> + <string name="error_status_source_load">Не вдалося завантажити джерело стану з сервера.</string> + <string name="post_media_alt">ALT</string> + <string name="action_discard">Відкинути зміни</string> + <string name="compose_unsaved_changes">У вас є незбережені зміни.</string> + <string name="action_continue_edit">Продовжити редагування</string> + <string name="mute_notifications_switch">Беззвучні сповіщення</string> + <string name="title_edits">Редагування</string> + <string name="status_edit_info">%1$s редагує</string> + <string name="status_created_info">%1$s створює</string> + <string name="a11y_label_loading_thread">Завантаження стрічки</string> + <string name="action_share_account_link">Поділитися посиланням на обліковий запис</string> + <string name="action_share_account_username">Поділитися іменем користувача облікового запису</string> + <string name="send_account_link_to">Поділитися URL облікового запису через…</string> + <string name="send_account_username_to">Поділитися іменем користувача облікового запису через…</string> + <string name="account_username_copied">Ім\'я користувача скопійовано</string> + <string name="pref_reading_order_newest_first">Спочатку новіші</string> + <string name="pref_title_reading_order">Порядок читання</string> + <string name="pref_reading_order_oldest_first">Спочатку давніші</string> + <string name="pref_summary_http_proxy_disabled">Вимкнено</string> + <string name="pref_summary_http_proxy_missing"><не налаштовано></string> + <string name="pref_summary_http_proxy_invalid"><недійсний></string> + <string name="action_browser_login">Увійти через браузер</string> + <string name="description_login">Працює в більшості випадків. Дані не витікають в інші застосунки.</string> + <string name="description_browser_login">Може підтримувати додаткові методи автентифікації, але для цього потрібен підтримуваний браузер.</string> + <string name="action_post_failed">Не вдалося вивантажити</string> + <string name="action_post_failed_show_drafts">Показати чернетки</string> + <string name="action_post_failed_do_nothing">Відхилити</string> + <string name="action_post_failed_detail">Не вдалося вивантажити ваш допис і його було збережено в чернетках. +\n +\nАбо з не вдалося зв\'язатися з сервером, або він відхилив допис.</string> + <string name="action_post_failed_detail_plural">Не вдалося вивантажити ваш допис і його було збережено в чернетках. +\n +\nАбо з не вдалося зв\'язатися з сервером, або він відхилив допис.</string> + <string name="accessibility_talking_about_tag">%1$d людей обговорюють хештеґ %2$s</string> + <string name="title_public_trending_hashtags">Популярні хештеги</string> + <string name="total_usage">Усього використано</string> + <string name="total_accounts">Усього облікових записів</string> + <string name="dialog_follow_hashtag_title">Стежити за хештегом</string> + <string name="dialog_follow_hashtag_hint">#hashtag</string> + <string name="action_refresh">Оновити</string> + <string name="notification_unknown_name">Невідомо</string> + <string name="socket_timeout_exception">Надто тривала спроба з\'єднання з вашим сервером</string> + <string name="ui_error_unknown">невідома причина</string> + <string name="ui_error_bookmark">Не вдалося додати допис до закладок: %1$s</string> + <string name="ui_error_clear_notifications">Не вдалося очистити сповіщення: %1$s</string> + <string name="ui_error_favourite">Не вдалося вподобати допис: %1$s</string> + <string name="ui_error_reblog">Не вдалося поширити допис: %1$s</string> + <string name="ui_error_vote">Не вдалося проголосувати: %1$s</string> + <string name="ui_error_accept_follow_request">Не вдалося погодити запит на стеження: %1$s</string> + <string name="ui_error_reject_follow_request">Не вдалося відхилити запит на стеження: %1$s</string> + <string name="ui_success_accepted_follow_request">Запит на стеження погоджено</string> + <string name="ui_success_rejected_follow_request">Запит на стеження заблоковано</string> + <string name="status_filtered_show_anyway">Усе одно показати</string> + <string name="status_filter_placeholder_label_format">Відфільтровано: %1$s</string> + <string name="pref_title_account_filter_keywords">Профілі</string> + <string name="label_filter_title">Заголовок</string> + <string name="filter_action_warn">Попередження</string> + <string name="filter_action_hide">Сховати</string> + <string name="filter_description_warn">Сховати з попередженням</string> + <string name="filter_description_hide">Сховати повністю</string> + <string name="label_filter_action">Фільтрувати дію</string> + <string name="label_filter_context">Фільтрувати контексти</string> + <string name="label_filter_keywords">Ключові слова або фрази для фільтрування</string> + <string name="action_add">Додати</string> + <string name="filter_keyword_display_format">%1$s (ціле слово)</string> + <string name="filter_keyword_addition_title">Додати ключове слово</string> + <string name="filter_edit_keyword_title">Змінити ключове слово</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="hint_filter_title">Мій фільтр</string> + <string name="pref_title_show_stat_inline">Показувати статистику допису у стрічці</string> + <string name="help_empty_home">Це ваша <b>головна стрічка</b>. Вона показує останні дописи облікових записів, за якими ви стежите. +\n +\nЩоб переглянути облікові записи, ви можете знайти їх в одній з інших стрічок. Наприклад, на локальній стрічці вашого сервера [iconics gmd_group]. Або ви можете шукати їх за іменами [iconics gmd_search]; наприклад, шукайте Tusky, щоб знайти наш обліковий запис Mastodon.</string> + <string name="post_media_image">Зображення</string> + <string name="select_list_manage">Керувати списками</string> + <string name="pref_ui_text_size">Розмір шрифту інтерфейсу</string> + <string name="notification_listenable_worker_name">Фонова діяльність</string> + <string name="notification_listenable_worker_description">Сповіщення, коли Tusky працює у фоновому режимі</string> + <string name="notification_notification_worker">Отримання сповіщень…</string> + <string name="notification_prune_cache">Обслуговування кешу…</string> + <string name="load_newest_notifications">Завантажити найновіші сповіщення</string> + <string name="compose_delete_draft">Видалити чернетку\?</string> + <string name="error_missing_edits">Ваш сервер знає, що цей допис було змінено, але не має копії редагувань, тому вони не можуть бути вам показані. +\n +\nЦе <a href="https://github.com/mastodon/mastodon/issues/25398">помилка #25398 у Mastodon</a>.</string> + <string name="error_media_upload_sending_fmt">Не вдалося вивантажити: %1$s</string> + <string name="about_device_info_title">Ваш пристрій</string> + <string name="about_device_info">%1$s %2$s +\nВерсія Android: %3$s +\nВерсія SDK: %4$d</string> + <string name="about_account_info_title">Ваш обліковий запис</string> + <string name="about_account_info">\@%1$s@%2$s +\nВерсія: %3$s</string> + <string name="about_copy">Скопіювати версію та відомості про пристрій</string> + <string name="about_copied">Версію та відомості про пристрій скопійовано</string> + <string name="list_exclusive_label">Сховати з домашньої стрічки</string> + <string name="help_empty_conversations">Тут розміщено ваші <b>приватні повідомлення</b>; іноді їх називають розмовами або прямими повідомленнями (DM). \u0020 +\n \u0020 +\nПриватні повідомлення створюються шляхом налаштування видимості [iconics gmd_public] допису на [iconics gmd_mail] <i>Особисту</i> і згадування в тексті одного або кількох користувачів. \u0020 +\n \u0020 +\nНаприклад, ви можете почати з перегляду профілю облікового запису, натиснути кнопку створення [iconics gmd_edit] і змінити видимість. \u0020</string> + <string name="error_media_playback">Не вдалося відтворити: %1$s</string> + <string name="dialog_delete_filter_text">Видалити фільтр \'%1$s\'\?</string> + <string name="dialog_delete_filter_positive_action">Видалити</string> + <string name="dialog_save_profile_changes_message">Хочете зберегти зміни у своєму профілі\?</string> + <string name="help_empty_lists">Це ваше <b>подання списків</b>. Ви можете створити кілька приватних списків і додати до них облікові записи. \u0020 +\n \u0020 +\nЗАУВАЖТЕ, що ви можете додавати до своїх списків лише ті облікові записи, на за якими слідкуєте. \u0020 +\n \u0020 +\nЦі списки можна використовувати як вкладку у налаштуваннях Вкладок облікового запису [iconics gmd_account_circle] [iconics gmd_navigate_next]. \u0020</string> + <string name="muting_hashtag_success_format">Сховати хештег #%1$s як попередження</string> + <string name="unmuting_hashtag_success_format">Показувати хештег #%1$s</string> + <string name="action_view_filter">Переглянути фільтр</string> + <string name="following_hashtag_success_format">Хештег #%1$s відстежується</string> + <string name="unfollowing_hashtag_success_format">Хештег #%1$s більше не відстежується</string> + <string name="error_blocking_domain">Не вдається сховати %1$s: %2$s</string> + <string name="error_unblocking_domain">Не вдалося показати %1$s: %2$s</string> + <string name="label_image">Зображення</string> + <string name="app_theme_system_black">Тема системи (чорна)</string> + <string name="title_public_trending_statuses">Популярні дописи</string> + <string name="pref_title_show_self_boosts">Показати самоцитування</string> + <string name="pref_title_show_self_boosts_description">Хтось цитує власний допис</string> + <string name="list_reply_policy_none">Ніхто</string> + <string name="list_reply_policy_list">Учасники списку</string> + <string name="list_reply_policy_followed">Будь-який користувач, за яким ви стежите</string> + <string name="list_reply_policy_label">Показати відповіді на</string> + <string name="pref_title_per_timeline_preferences">Налаштування окремих стрічок</string> + <string name="pref_title_show_notifications_filter">Показати фільтр сповіщень</string> + <string name="reply_sending">Надсилання…</string> + <string name="reply_sending_long">Вашу відповідь надіслано.</string> + <string name="action_translate">Перекласти</string> + <string name="action_show_original">Показати оригінал</string> + <string name="label_translating">Перекладається…</string> + <string name="label_translated">Перекладено з %1$s на %2$s</string> + <string name="ui_error_translate">Неможливо перекласти: %1$s</string> + <string name="report_category_legal">Правові суперечності</string> + <string name="unknown_notification_type">Невідомий тип сповіщення</string> + <string name="url_copied">Url скопійовано</string> + <string name="confirmation_hashtag_copied">\'#%1$s\' скопійовано</string> + <string name="dialog_follow_warning">Стежити за обліковим записом?</string> + <string name="pref_title_confirm_follows">Запит підтвердження перед початком стеження</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-v27/styles.xml b/app/src/main/res/values-v27/styles.xml new file mode 100644 index 0000000..586c7f3 --- /dev/null +++ b/app/src/main/res/values-v27/styles.xml @@ -0,0 +1,16 @@ +<resources> + + <style name="TuskyTheme" parent="TuskyBaseTheme"> + <item name="android:windowLightNavigationBar">@bool/lightNavigationBar</item> + <item name="android:navigationBarColor">@color/colorBackground</item> + <item name="android:navigationBarDividerColor">?attr/dividerColor</item> + </style> + + <!--Black Application Theme Styles--> + <style name="TuskyBlackTheme" parent="TuskyBlackThemeBase"> + <item name="android:windowLightNavigationBar">false</item> + <item name="android:navigationBarColor">@color/black</item> + <item name="android:navigationBarDividerColor">?attr/dividerColor</item> + </style> + +</resources> diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml new file mode 100644 index 0000000..0d93829 --- /dev/null +++ b/app/src/main/res/values-vi/strings.xml @@ -0,0 +1,705 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="notification_clear_text">Bạn có muốn xóa toàn bộ thông báo\?</string> + <string name="send_post_notification_saved_content">Đã lưu tút vào nháp</string> + <string name="send_post_notification_cancel_title">Hủy đăng</string> + <string name="send_post_notification_channel_name">Đăng Tút</string> + <string name="send_post_notification_title">Đang đăng…</string> + <plurals name="notification_title_summary"> + <item quantity="other">%1$d thông báo mới</item> + </plurals> + <string name="notification_summary_small">%1$s và %2$s</string> + <string name="notification_summary_medium">%1$s, %2$s, và %3$s</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s và %4$d người</string> + <string name="notification_mention_format">%1$s nhắc tới bạn</string> + <string name="notification_mention_name">Nhắc đến tôi</string> + <string name="notification_follow_request_format">%1$s yêu cầu theo dõi bạn</string> + <string name="notification_follow_format">%1$s theo dõi bạn</string> + <string name="notification_favourite_format">%1$s thích tút của bạn</string> + <string name="notification_reblog_format">%1$s đăng lại tút của bạn</string> + <string name="post_lookup_error_format">Lỗi khi tìm tút %1$s</string> + <string name="error_no_custom_emojis">Máy chủ %1$s không có emoji tùy chỉnh</string> + <string name="send_post_notification_error_title">Lỗi đăng tút</string> + <string name="error_delete_list">Không thể xóa danh sách</string> + <string name="error_rename_list">Không thể cập nhật danh sách</string> + <string name="error_create_list">Không thể tạo danh sách</string> + <string name="error_sender_account_gone">Xảy ra lỗi khi đăng tút.</string> + <string name="error_media_upload_sending">Tải lên không thành công.</string> + <string name="error_media_upload_image_or_video">Không thể đính kèm ảnh và video cùng một lúc.</string> + <string name="error_media_download_permission">Cần có quyền truy cập bộ sưu tập.</string> + <string name="error_media_upload_permission">Cần có quyền đọc tập tin.</string> + <string name="error_media_upload_opening">Không thể mở tập tin.</string> + <string name="error_media_upload_type">Không hỗ trợ định dạng này.</string> + <string name="error_compose_character_limit">Vượt quá số ký tự cho phép!</string> + <string name="error_retrieving_oauth_token">Lấy token đăng nhập thất bại.</string> + <string name="error_authorization_denied">Truy cập bị từ chối.</string> + <string name="error_authorization_unknown">Xảy ra lỗi khi cố gắng truy cập.</string> + <string name="error_no_web_browser_found">Không tìm thấy trình duyệt web.</string> + <string name="error_invalid_domain">Tên miền không hợp lệ</string> + <string name="error_empty">Không được để trống.</string> + <string name="error_network">Rớt mạng! Xin kiểm tra kết nối và thử lại!</string> + <string name="error_generic">Đã có lỗi xảy ra.</string> + <string name="error_failed_app_registration">Máy chủ này không cấp quyền truy cập.</string> + <string name="title_lists">Danh sách</string> + <string name="action_lists">Danh sách</string> + <string name="about_title_activity">Thông tin</string> + <string name="action_reset_schedule">Làm tươi</string> + <string name="action_search">Tìm kiếm</string> + <string name="action_edit_profile">Hồ sơ</string> + <string name="action_view_account_preferences">Cá nhân</string> + <string name="action_view_preferences">Cài đặt</string> + <string name="action_logout">Đăng xuất</string> + <string name="button_done">Xong</string> + <string name="action_send_public">ĐĂNG</string> + <string name="button_back">Quay lại</string> + <string name="button_continue">Tiếp tục</string> + <string name="filter_dialog_update_button">Cập nhật</string> + <string name="filter_dialog_remove_button">Xóa</string> + <string name="action_send">ĐĂNG</string> + <string name="action_login">Đăng nhập Mastodon</string> + <string name="dialog_redraft_post_warning">Xóa và viết lại tút này\?</string> + <string name="dialog_delete_post_warning">Xóa tút này\?</string> + <string name="dialog_unfollow_warning">Bỏ theo dõi người này\?</string> + <string name="dialog_message_cancel_follow_request">Hủy yêu cầu theo dõi\?</string> + <string name="dialog_download_image">Tải về</string> + <string name="dialog_message_uploading_media">Đang tải…</string> + <string name="dialog_title_finishing_media_upload">Đã tải xong tập tin</string> + <string name="dialog_whats_an_instance">Bạn phải nhập một tên miền. Ví dụ mastodon.social, icosahedron.website, social.tchncs.de và <a href="https://instances.social">vô số khác!</a> +\n +\nNếu chưa có tài khoản, bạn phải tạo tài khoản ở đó trước. +\n +\nMáy chủ, nói cách khác là một cộng đồng nơi mà bạn đăng ký tài khoản trên đó, nhưng bạn vẫn có thể dễ dàng giao tiếp và theo dõi mọi người trên các máy chủ khác. +\n +\nTham khảo <a href="https://joinmastodon.org">joinmastodon.org</a> </string> + <string name="login_connection">Đang kết nối…</string> + <string name="label_header">Ảnh bìa</string> + <string name="label_avatar">Ảnh đại diện</string> + <string name="label_quick_reply">Trả lời…</string> + <string name="search_no_results">Không tìm thấy</string> + <string name="hint_search">Tìm kiếm…</string> + <string name="hint_note">Giới thiệu</string> + <string name="hint_display_name">Biệt danh</string> + <string name="hint_content_warning">Nội dung bạn muốn ẩn</string> + <string name="hint_compose">Bạn đang nghĩ về điều gì\?</string> + <string name="hint_domain">Bạn ở máy chủ nào\?</string> + <string name="confirmation_domain_unmuted">Đã bỏ ẩn %1$s</string> + <string name="confirmation_unmuted">Đã bỏ ẩn người này</string> + <string name="confirmation_unblocked">Đã bỏ chặn người này</string> + <string name="confirmation_reported">Đã gửi!</string> + <string name="send_media_to">Chia sẻ tập tin với…</string> + <string name="send_post_link_to">Đăng lại URL tút với…</string> + <string name="send_post_content_to">Đăng lại tút với…</string> + <string name="downloading_media">Đang lưu vào thiết bị</string> + <string name="download_media">Tải xuống</string> + <string name="action_share_as">Đăng lại với tư cách …</string> + <string name="action_open_as">Mở với tư cách %1$s</string> + <string name="action_copy_link">Chép URL</string> + <string name="download_image">Đang tải %1$s</string> + <string name="action_open_media_n">Mở tập tin #%1$d</string> + <string name="title_links_dialog">Links</string> + <string name="title_mentions_dialog">Lượt nhắc tới</string> + <string name="title_hashtags_dialog">Hashtag</string> + <string name="action_open_faved_by">Xem lượt thích</string> + <string name="action_open_reblogged_by">Xem lượt đăng lại</string> + <string name="action_open_reblogger">Xem lượt đăng lại</string> + <string name="action_hashtags">Hashtag</string> + <string name="action_mentions">Lượt nhắc tới</string> + <string name="action_links">Links</string> + <string name="action_add_tab">Thêm Tab</string> + <string name="action_schedule_post">Lên lịch</string> + <string name="action_emoji_keyboard">Emoji</string> + <string name="action_content_warning">Nội dung ẩn</string> + <string name="action_toggle_visibility">Công khai</string> + <string name="action_access_scheduled_posts">Đăng tự động</string> + <string name="action_access_drafts">Nháp</string> + <string name="action_reject">Từ chối</string> + <string name="action_accept">Đồng ý</string> + <string name="action_undo">Trở về</string> + <string name="action_edit_own_profile">Sửa</string> + <string name="action_save">Lưu</string> + <string name="action_open_drawer">Mở ngăn kéo</string> + <string name="action_hide_media">Làm mờ hình ảnh</string> + <string name="action_mention">Nhắc tới</string> + <string name="action_unmute_conversation">Mở lại thông báo</string> + <string name="action_mute_conversation">Tắt thông báo</string> + <string name="action_mute_domain">Ẩn %1$s</string> + <string name="action_unmute">Bỏ ẩn</string> + <string name="action_mute">Ẩn</string> + <string name="action_share">Chia sẻ</string> + <string name="action_photo_take">Chụp hình</string> + <string name="action_add_poll">Tạo bình chọn</string> + <string name="action_add_media">Thêm tệp</string> + <string name="action_open_in_web">Mở trong trình duyệt</string> + <string name="action_view_media">Media</string> + <string name="action_view_follow_requests">Yêu cầu theo dõi</string> + <string name="action_view_domain_mutes">Những máy chủ đã ẩn</string> + <string name="action_view_blocks">Những người đã chặn</string> + <string name="action_view_mutes">Những người đã ẩn</string> + <string name="action_view_bookmarks">Lưu</string> + <string name="action_view_favourites">Thích</string> + <string name="action_view_profile">Trang hồ sơ</string> + <string name="action_close">Đóng</string> + <string name="action_retry">Thử lại</string> + <string name="action_delete_and_redraft">Xóa & viết lại</string> + <string name="action_delete">Xóa</string> + <string name="action_edit">Sửa</string> + <string name="action_report">Báo cáo</string> + <string name="action_show_reblogs">Hiện lượt đăng lại</string> + <string name="action_hide_reblogs">Ẩn lượt đăng lại</string> + <string name="action_unblock">Bỏ chặn</string> + <string name="action_block">Chặn</string> + <string name="action_unfollow">Bỏ theo dõi</string> + <string name="action_follow">Theo dõi</string> + <string name="action_logout_confirm">Bạn có muốn đăng xuất tài khoản %1$s\? Dữ liệu của tài khoản sẽ bị xóa, bao gồm những bản nháp và thiết lập.</string> + <string name="action_compose">Soạn tút</string> + <string name="action_more">Thêm</string> + <string name="action_unfavourite">Bỏ lưu</string> + <string name="action_bookmark">Lưu</string> + <string name="action_favourite">Thích</string> + <string name="action_unreblog">Hủy đăng lại</string> + <string name="action_reblog">Đăng lại</string> + <string name="action_reply">Trả lời</string> + <string name="action_quick_reply">Trả lời nhanh</string> + <string name="report_comment_hint">Ghi chú\?</string> + <string name="report_username_format">Báo cáo @%1$s</string> + <string name="footer_empty">Trượt xuống để tải nội dung!</string> + <string name="message_empty">Trống.</string> + <string name="post_content_show_less">Thu gọn</string> + <string name="post_content_show_more">Tiếp tục đọc</string> + <string name="post_content_warning_show_less">Thu gọn</string> + <string name="post_content_warning_show_more">Mở rộng</string> + <string name="post_sensitive_media_directions">Hiển thị</string> + <string name="post_media_hidden_title">Media bị ẩn</string> + <string name="post_sensitive_media_title">Nhạy cảm</string> + <string name="post_boosted_format">%1$s đăng lại</string> + <string name="post_username_format">\@%1$s</string> + <string name="title_licenses">Giấy phép</string> + <string name="title_scheduled_posts">Những tút đã lên lịch</string> + <string name="title_edit_profile">Chỉnh sửa hồ sơ</string> + <string name="title_follow_requests">Yêu cầu theo dõi</string> + <string name="title_domain_mutes">Những máy chủ đã ẩn</string> + <string name="title_blocks">Những người đã chặn</string> + <string name="title_mutes">Những người đã ẩn</string> + <string name="title_bookmarks">Những tút đã lưu</string> + <string name="title_followers">Người theo dõi</string> + <string name="title_follows">Theo dõi</string> + <string name="title_posts_pinned">Ghim</string> + <string name="title_posts_with_replies">Lượt trả lời</string> + <string name="title_posts">Tút</string> + <string name="title_view_thread">Nội dung tút</string> + <string name="title_tab_preferences">Xếp tab</string> + <string name="title_direct_messages">Nhắn riêng</string> + <string name="title_public_federated">Liên hợp</string> + <string name="title_public_local">Máy chủ</string> + <string name="title_notifications">Thông báo</string> + <string name="title_home">Trang chính</string> + <string name="title_drafts">Những tút nháp</string> + <string name="title_favourites">Những tút đã thích</string> + <string name="link_whats_an_instance">Máy chủ là gì\?</string> + <string name="pref_title_show_media_preview">Hiện xem trước hình ảnh</string> + <string name="pref_title_show_replies">Hiện những trả lời</string> + <string name="pref_title_show_boosts">Hiện lượt đăng lại</string> + <string name="pref_title_post_tabs">Trang chủ</string> + <string name="pref_title_post_filter">Lọc bảng tin</string> + <string name="pref_title_gradient_for_media">Phủ màu media nhạy cảm</string> + <string name="pref_title_animate_gif_avatars">Ảnh đại diện GIF</string> + <string name="pref_title_bot_overlay">Icon cho tài khoản Bot</string> + <string name="pref_title_language">Ngôn ngữ</string> + <string name="pref_title_custom_tabs">Mở luôn trong app</string> + <string name="pref_title_browser_settings">Trình duyệt</string> + <string name="app_theme_system">Mặc định của thiết bị</string> + <string name="app_theme_auto">Tự động khi trời tối</string> + <string name="app_theme_black">Đen</string> + <string name="app_theme_light">Sáng</string> + <string name="app_them_dark">Tối</string> + <string name="pref_title_timeline_filters">Bộ lọc</string> + <string name="pref_title_timelines">Bảng tin</string> + <string name="pref_title_app_theme">Chủ đề</string> + <string name="pref_title_appearance_settings">Giao diện</string> + <string name="pref_title_notification_filter_poll">cuộc bình chọn kết thúc</string> + <string name="pref_title_notification_filter_favourites">tút được thích</string> + <string name="pref_title_notification_filter_reblogs">tút được đăng lại</string> + <string name="pref_title_notification_filter_follow_requests">yêu cầu theo dõi</string> + <string name="pref_title_notification_filter_follows">lượt theo dõi</string> + <string name="pref_title_notification_filter_mentions">nhắc tới</string> + <string name="pref_title_notification_filters">Thông báo tôi khi</string> + <string name="pref_title_notification_alert_light">Kèm đèn sáng</string> + <string name="pref_title_notification_alert_vibrate">Kèm rung</string> + <string name="pref_title_notification_alert_sound">Kèm theo tiếng bíp</string> + <string name="pref_title_notification_alerts">Báo động</string> + <string name="pref_title_notifications_enabled">Thông báo</string> + <string name="pref_title_edit_notification_settings">Thông báo</string> + <string name="visibility_direct">Nhắn riêng: Chỉ người được nhắc đến</string> + <string name="visibility_private">Chỉ người theo dõi</string> + <string name="visibility_unlisted">Hạn chế: Ẩn trên bảng tin</string> + <string name="visibility_public">Công khai: Mọi người đều thấy</string> + <string name="dialog_mute_warning">Ẩn @%1$s\?</string> + <string name="dialog_block_warning">Chặn @%1$s\?</string> + <string name="mute_domain_warning_dialog_ok">Ẩn máy chủ này</string> + <string name="mute_domain_warning">Bạn có muốn ẩn %1$s\? Bạn sẽ không thấy bất kỳ nội dung nào từ máy chủ này nữa. Người theo dõi bạn ở máy chủ này cũng sẽ bị xóa.</string> + <string name="notification_follow_request_name">Yêu cầu theo dõi</string> + <string name="notification_follow_description">Thông báo về người theo dõi mới</string> + <string name="notification_follow_name">Người theo dõi mới</string> + <string name="notification_mention_descriptions">Thông báo về lượt nhắc tới</string> + <string name="post_text_size_largest">To nhất</string> + <string name="post_text_size_large">To</string> + <string name="post_text_size_medium">Trung bình</string> + <string name="post_text_size_small">Nhỏ vừa</string> + <string name="post_text_size_smallest">Nhỏ</string> + <string name="pref_post_text_size">Cỡ chữ tút</string> + <string name="post_privacy_followers_only">Chỉ người theo dõi</string> + <string name="post_privacy_unlisted">Hạn chế</string> + <string name="post_privacy_public">Công khai</string> + <string name="pref_main_nav_position_option_bottom">Dưới màn hình</string> + <string name="pref_main_nav_position_option_top">Trên màn hình</string> + <string name="pref_main_nav_position">Vị trí menu</string> + <string name="pref_failed_to_sync">Không thể lưu thiết lập</string> + <string name="pref_publishing">Đăng</string> + <string name="pref_default_media_sensitivity">Tài khoản nhạy cảm (đồng bộ máy chủ)</string> + <string name="pref_default_post_privacy">Kiểu tút mặc định (đồng bộ máy chủ)</string> + <string name="pref_title_http_proxy_server">Máy chủ proxy</string> + <string name="pref_title_http_proxy_port">Cổng</string> + <string name="pref_title_http_proxy_enable">Bật proxy</string> + <string name="pref_title_http_proxy_settings">Dùng proxy</string> + <string name="pref_title_proxy_settings">Vượt tường lửa</string> + <string name="notification_boost_description">Thông báo khi tút của bạn được đăng lại</string> + <string name="notification_boost_name">Đăng lại</string> + <string name="notification_follow_request_description">Thông báo về lượt yêu cầu theo dõi</string> + <string name="about_bug_feature_request_site">Báo lỗi và đề xuất tính năng +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_project_site">Website dự án: https://tusky.app</string> + <string name="about_tusky_license">Tusky là phần mềm mã nguồn mở, được phân phối với giấy phép GNU General Public License Version 3. Bạn có thể tham khảo thêm tại: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <string name="about_powered_by_tusky">Powered by Tusky</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="description_account_locked">Tài khoản bị khóa</string> + <string name="notification_poll_description">Thông báo khi một cuộc bình chọn kết thúc</string> + <string name="notification_poll_name">Bình chọn</string> + <string name="notification_favourite_description">Thông báo khi ai đó thích tút của bạn</string> + <string name="notification_favourite_name">Lượt thích</string> + <string name="filter_dialog_whole_word">Toàn bộ chữ có chứa cụm từ này</string> + <string name="filter_edit_title">Sửa bộ lọc</string> + <string name="filter_addition_title">Thêm bộ lọc</string> + <string name="pref_title_thread_filter_keywords">Thảo luận</string> + <string name="pref_title_public_filter_keywords">Liên hợp</string> + <string name="load_more_placeholder_text">hiện những tút chưa đọc</string> + <string name="replying_to">Trả lời đến @%1$s</string> + <string name="title_media">Media</string> + <string name="pref_title_alway_open_spoiler">Hiện nội dung ẩn</string> + <string name="pref_title_alway_show_sensitive_media">Hiện nội dung nhạy cảm</string> + <string name="follows_you">Đang theo dõi bạn</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + <string name="abbreviated_minutes_ago">%1$d phút</string> + <string name="abbreviated_hours_ago">%1$d giờ</string> + <string name="abbreviated_days_ago">%1$d ngày</string> + <string name="abbreviated_years_ago">%1$d năm</string> + <string name="abbreviated_in_seconds">%1$ds</string> + <string name="abbreviated_in_minutes">%1$d phút</string> + <string name="abbreviated_in_hours">%1$d giờ</string> + <string name="abbreviated_in_days">%1$d ngày</string> + <string name="abbreviated_in_years">%1$d năm</string> + <string name="state_follow_requested">Yêu cầu theo dõi</string> + <string name="post_media_video">Video</string> + <string name="post_media_images">Hình ảnh</string> + <string name="post_share_link">URL tút</string> + <string name="post_share_content">Nội dung tút</string> + <string name="about_tusky_account">Trang hồ sơ Tusky</string> + <string name="pref_title_confirm_reblogs">Hỏi trước khi đăng lại tút</string> + <string name="pref_title_show_cards_in_timelines">Hiện xem trước link</string> + <string name="warning_scheduling_interval">Mastodon giới hạn tối thiểu 5 phút.</string> + <string name="no_scheduled_posts">Bạn không có tút đã lên lịch.</string> + <string name="no_drafts">Bạn không có tút nháp.</string> + <string name="edit_poll">Sửa</string> + <string name="poll_new_choice_hint">Lựa chọn %1$d</string> + <string name="poll_allow_multiple_choices">Cho phép chọn nhiều lựa chọn</string> + <string name="add_poll_choice">Thêm</string> + <string name="duration_7_days">7 ngày</string> + <string name="duration_3_days">3 ngày</string> + <string name="duration_1_day">1 ngày</string> + <string name="duration_6_hours">6 giờ</string> + <string name="duration_1_hour">1 giờ</string> + <string name="duration_30_min">30 phút</string> + <string name="duration_5_min">5 phút</string> + <string name="create_poll_title">Bình chọn</string> + <string name="pref_title_enable_swipe_for_tabs">Vuốt qua lại giữa các tab</string> + <string name="failed_search">Không thể tìm thấy</string> + <string name="title_accounts">Người</string> + <string name="report_description_remote_instance">Người này thuộc máy chủ khác. Gửi luôn cho máy chủ đó\?</string> + <string name="report_description_1">Báo cáo này sẽ được gửi tới kiểm duyệt viên. Hãy cho biết lý do vì sao bạn báo cáo người này bên dưới:</string> + <string name="failed_fetch_posts">Chưa tải được tút</string> + <string name="failed_report">Báo cáo thất bại</string> + <string name="report_remote_instance">Gửi cho %1$s</string> + <string name="hint_additional_info">Thêm ghi chú</string> + <string name="report_sent_success">Đã gửi báo cáo @%1$s</string> + <plurals name="poll_timespan_seconds"> + <item quantity="other">%1$d giây</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="other">%1$d phút</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="other">%1$d giờ</item> + </plurals> + <plurals name="poll_timespan_days"> + <item quantity="other">%1$d ngày</item> + </plurals> + <string name="poll_ended_created">Cuộc bình chọn của bạn đã kết thúc</string> + <string name="poll_ended_voted">Cuộc bình chọn đã kết thúc</string> + <string name="poll_vote">Bình chọn</string> + <string name="poll_info_closed">xong</string> + <string name="poll_info_time_absolute">kết thúc lúc %1$s</string> + <plurals name="poll_info_people"> + <item quantity="other">%1$s người bình chọn</item> + </plurals> + <plurals name="poll_info_votes"> + <item quantity="other">%1$s người bình chọn</item> + </plurals> + <string name="poll_info_format"> \u0020<!-- 15 bình chọn • còn 1 giờ --> \u0020%1$s • %2$s</string> + <string name="compose_preview_image_description">Mô tả cho hình %1$s</string> + <string name="compose_shortcut_short_label">Soạn</string> + <string name="compose_shortcut_long_label">Soạn tút</string> + <string name="filter_apply">Áp dụng</string> + <string name="notifications_apply_filter">Lọc</string> + <string name="notifications_clear">Xóa hết</string> + <string name="list">Danh sách</string> + <string name="select_list_title">Chọn danh sách</string> + <string name="hashtags">Hashtag</string> + <string name="edit_hashtag_hint">Không cần dấu #</string> + <string name="add_hashtag_title">Thêm hashtag</string> + <string name="hint_list_name">Tên danh sách</string> + <string name="description_poll">Lượt bình chọn: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="description_visibility_direct">Nhắn riêng</string> + <string name="description_visibility_private">Người theo dõi</string> + <string name="description_visibility_unlisted">Hạn chế</string> + <string name="description_visibility_public">Công khai</string> + <string name="description_post_bookmarked">Đã lưu</string> + <string name="description_post_favourited">Đã thích</string> + <string name="description_post_reblogged">Đã đăng lại</string> + <string name="description_post_media_no_description_placeholder">Không có mô tả</string> + <string name="description_post_cw">Nội dung ẩn: %1$s</string> + <string name="conversation_more_recipients">%1$s, %2$s và %3$d người nữa</string> + <string name="conversation_2_recipients">%1$s và %2$s</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="title_favourited_by">Lượt thích tút này</string> + <string name="title_reblogged_by">Lượt đăng lại tút này</string> + <plurals name="reblogs"> + <item quantity="other"><b>%1$s</b> Đăng lại</item> + </plurals> + <plurals name="favs"> + <item quantity="other"><b>%1$s</b> Thích</item> + </plurals> + <string name="pin_action">Ghim</string> + <string name="unpin_action">Gỡ ghim</string> + <string name="label_remote_account">Nội dung có thể hiển thị không đầy đủ. Nhấn để mở xem chi tiết trên trình duyệt.</string> + <string name="pref_title_absolute_time">Sử dụng thời gian thiết bị</string> + <string name="profile_metadata_content_label">Nội dung</string> + <string name="profile_metadata_label_label">Nhãn</string> + <string name="profile_metadata_add">thêm nội dung</string> + <string name="profile_metadata_label">Metadata</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_apache_2">Giấy phép Apache (xem bên dưới)</string> + <string name="license_description">Tusky có sử dụng mã nguồn từ những dự án mã nguồn mở sau:</string> + <string name="unreblog_private">Hủy đăng lại</string> + <string name="reblog_private">Đăng lại công khai</string> + <string name="account_moved_description">%1$s đã chuyển sang:</string> + <string name="profile_badge_bot_text">Tài khoản Bot</string> + <string name="download_failed">Tải về thất bại</string> + <string name="caption_notoemoji">Emoji của Google</string> + <string name="caption_twemoji">Emoji chính thức của Mastodon</string> + <string name="caption_blobmoji">Emoji của Android phiên bản từ 4.4 đến 7.1</string> + <string name="caption_systememoji">Sử dụng emoji mặc định của thiết bị</string> + <string name="restart">Khởi động lại</string> + <string name="later">Để sau</string> + <string name="restart_emoji">Bạn cần khởi động lại Tusky để áp dụng các thiết lập</string> + <string name="restart_required">Yêu cầu khởi động lại ứng dụng</string> + <string name="action_open_post">Đọc tút</string> + <string name="expand_collapse_all_posts">Mở rộng/Thu gọn toàn bộ tút</string> + <string name="performing_lookup_title">Đang tra cứu…</string> + <string name="download_fonts">Bạn cần tải về bộ emoji này trước</string> + <string name="system_default">Mặc định của thiết bị</string> + <string name="emoji_style">Emoji</string> + <string name="action_compose_shortcut">Soạn</string> + <string name="compose_save_draft">Lưu nháp\?</string> + <string name="lock_account_label_description">Tự bạn sẽ phê duyệt người theo dõi</string> + <string name="lock_account_label">Tài khoản riêng tư</string> + <string name="action_remove">Hủy bỏ</string> + <string name="action_set_caption">Mô tả</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">Mô tả dành cho người khiếm thị +\n(Tối đa %1$d ký tự)</item> + </plurals> + <string name="compose_active_account_description">Đăng với tư cách %1$s</string> + <string name="action_add_to_list">Thêm người này vào danh sách</string> + <string name="action_remove_from_list">Xóa người này khỏi danh sách</string> + <string name="hint_search_people_list">Tìm người bạn đã theo dõi</string> + <string name="action_delete_list">Xóa danh sách</string> + <string name="action_rename_list">Đổi tên danh sách</string> + <string name="action_create_list">Tạo danh sách</string> + <string name="add_account_description">Thêm tài khoản Mastodon</string> + <string name="add_account_name">Thêm tài khoản</string> + <string name="filter_add_description">Cụm từ muốn lọc</string> + <string name="filter_dialog_whole_word_description">Chỉ có tác dụng nếu cụm từ là chữ-số trùng khớp, viết liền không dấu cách</string> + <string name="description_post_media">Media: %1$s</string> + <string name="dialog_mute_hide_notifications">Ẩn thông báo</string> + <string name="action_unmute_desc">Bỏ ẩn %1$s</string> + <string name="action_unmute_domain">Bỏ ẩn %1$s</string> + <string name="pref_title_hide_top_toolbar">Ẩn tiêu đề tab</string> + <string name="account_note_saved">Đã lưu!</string> + <string name="account_note_hint">Ghi chú về người này</string> + <string name="no_announcements">Chưa có thông báo.</string> + <string name="title_announcements">Có gì mới\?</string> + <string name="wellbeing_hide_stats_profile">Ẩn số liệu trên trang hồ sơ</string> + <string name="wellbeing_hide_stats_posts">Ẩn số liệu trên tút</string> + <string name="limit_notifications">Giảm bớt thông báo</string> + <string name="review_notifications">Chọn loại thông báo</string> + <string name="wellbeing_mode_notice">Các thông tin ảnh hưởng tới tâm lý hành vi của bạn sẽ bị ẩn. Bao gồm: +\n +\n - Thông báo Lượt thích/Đăng lại/Theo dõi +\n - Số Lượt thích/Đăng lại tút +\n - Số Người theo dõi/Tút trên trang hồ sơ +\n +\nThông báo đẩy sẽ không ảnh hưởng, bạn có thể tự thiết lập trong phần cài đặt điện thoại của bạn.</string> + <string name="pref_title_wellbeing_mode">Chống nghiện</string> + <string name="notification_subscription_description">Thông báo khi người bạn theo dõi đăng tút mới</string> + <string name="notification_subscription_name">Tút mới</string> + <string name="pref_title_notification_filter_subscriptions">người tôi đăng ký theo dõi đăng tút mới</string> + <string name="notification_subscription_format">%1$s đăng tút mới</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="other">Bạn không thể đính kèm quá %1$d tệp.</item> + </plurals> + <string name="duration_indefinite">Vĩnh viễn</string> + <string name="label_duration">Thời hạn</string> + <string name="dialog_delete_list_warning">Bạn thật sự muốn xóa danh sách %1$s\?</string> + <string name="post_media_attachments">Đính kèm</string> + <string name="post_media_audio">Âm thanh</string> + <string name="drafts_post_reply_removed">Đã xóa tút trả lời nháp</string> + <string name="draft_deleted">Đã xóa tút lên lịch</string> + <string name="drafts_failed_loading_reply">Chưa tải được bình luận</string> + <string name="drafts_post_failed_to_send">Đăng tút không thành công!</string> + <string name="pref_title_animate_custom_emojis">Emoji GIF</string> + <string name="action_unsubscribe_account">Dừng nhận thông báo</string> + <string name="action_subscribe_account">Nhận thông báo</string> + <string name="follow_requests_info">Dù biết tài khoản của bạn công khai, quản trị viên %1$s vẫn nghĩ bạn hãy nên duyệt thủ công yêu cầu theo dõi từ những người lạ.</string> + <string name="dialog_delete_conversation_warning">Xóa cuộc thảo luận này\?</string> + <string name="action_delete_conversation">Xóa thảo luận</string> + <string name="pref_title_confirm_favourites">Hỏi trước khi thích tút</string> + <string name="action_unbookmark">Bỏ lưu</string> + <string name="duration_14_days">14 ngày</string> + <string name="duration_30_days">30 ngày</string> + <string name="duration_60_days">60 ngày</string> + <string name="duration_90_days">90 ngày</string> + <string name="duration_180_days">180 ngày</string> + <string name="duration_365_days">365 ngày</string> + <string name="tusky_compose_post_quicksetting_label">Soạn tút</string> + <string name="pref_title_notification_filter_sign_ups">ai đó mới tham gia máy chủ</string> + <string name="notification_sign_up_format">%1$s tham gia máy chủ</string> + <string name="notification_sign_up_name">Thành viên mới</string> + <string name="notification_sign_up_description">Thông báo về người mới tham gia máy chủ</string> + <string name="notification_update_format">%1$s đã sửa tút của họ</string> + <string name="pref_title_notification_filter_updates">khi một tút mà tôi tương tác bị sửa</string> + <string name="notification_update_name">Sửa tút</string> + <string name="notification_update_description">Thông báo khi tút mà tôi tương tác bị sửa</string> + <string name="title_login">Đăng nhập</string> + <string name="error_could_not_load_login_page">Không thể tải trang đăng nhập.</string> + <string name="saving_draft">Đang lưu nháp…</string> + <string name="action_dismiss">Bỏ qua</string> + <string name="title_migration_relogin">Đăng nhập lại để hiện thông báo đẩy</string> + <string name="action_details">Chi tiết</string> + <string name="account_date_joined">Đã tham gia %1$s</string> + <string name="tips_push_notification_migration">Đăng nhập lại tất cả tài khoản để kích hoạt thông báo đẩy.</string> + <string name="dialog_push_notification_migration_other_accounts">Bạn đã đăng nhập lại vào tài khoản hiện tại của mình để cấp quyền thông báo đẩy cho Tusky. Tuy nhiên, bạn vẫn có các tài khoản khác chưa kích hoạt thông báo đẩy theo cách này. Chuyển sang chúng và đăng nhập từng cái một để cho phép hỗ trợ thông báo UnifiedPush.</string> + <string name="dialog_push_notification_migration">Để sử dụng thông báo đẩy qua UnifiedPush, Tusky cần có quyền đăng ký thông báo trên máy chủ Mastodon của bạn. Bạn hãy thoát ra rồi đăng nhập lại để thay đổi phạm vi OAuth được cấp cho Tusky. Sử dụng đăng nhập lại ở đây hoặc trong cài đặt Tài khoản sẽ bảo toàn tất cả các tút nháp và bộ nhớ đệm trên điện thoại của bạn.</string> + <string name="status_count_one_plus">1+</string> + <string name="action_edit_image">Sửa ảnh</string> + <string name="error_image_edit_failed">Hình ảnh này không thể sửa.</string> + <string name="error_loading_account_details">Không thể tải thông tin tài khoản</string> + <string name="error_multimedia_size_limit">Video và audio không thể quá %1$s MB.</string> + <string name="error_following_hashtag_format">Lỗi khi theo dõi #%1$s</string> + <string name="error_unfollowing_hashtag_format">Lỗi khi bỏ theo dõi #%1$s</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="duration_no_change">(Không đổi)</string> + <string name="description_post_language">Ngôn ngữ đăng</string> + <string name="action_set_focus">Chọn tâm điểm</string> + <string name="set_focus_description">Nhấn hoặc kéo vòng tròn để chọn tiêu điểm sẽ hiển thị trong hình thu nhỏ.</string> + <string name="pref_title_show_self_username">Hiện URL của tôi trên tab</string> + <string name="pref_show_self_username_always">Luôn luôn</string> + <string name="pref_show_self_username_disambiguate">Khi đăng nhập nhiều tài khoản</string> + <string name="pref_show_self_username_never">Không hiện</string> + <string name="delete_scheduled_post_warning">Bạn có chắc muốn xóa tút đã lên lịch\?</string> + <string name="instance_rule_info">Đăng nhập nghĩa là bạn đồng ý với nội quy của %1$s.</string> + <string name="instance_rule_title">Nội quy của %1$s</string> + <string name="compose_save_draft_loses_media">Lưu bản nháp\? (Bạn sẽ cần tải lên lại file đính kèm)</string> + <string name="failed_to_unpin">Không thể bỏ ghim</string> + <string name="failed_to_pin">Không thể ghim</string> + <string name="action_add_reaction">biểu cảm</string> + <string name="notification_report_description">Thông báo báo cáo kiểm duyệt</string> + <string name="action_add_or_remove_from_list">Sửa danh sách</string> + <string name="failed_to_add_to_list">Không thể thêm người vào danh sách</string> + <string name="failed_to_remove_from_list">Không thể xoá người khỏi danh sách</string> + <string name="action_unfollow_hashtag_format">Bỏ theo dõi #%1$s\?</string> + <string name="status_created_at_now">vừa xong</string> + <string name="notification_report_format">Báo cáo mới trên %1$s</string> + <string name="notification_header_report_format">%1$s báo cáo %2$s</string> + <string name="notification_summary_report_format">%1$s trước · %2$d tút</string> + <string name="confirmation_hashtag_unfollowed">Đã bỏ theo dõi #%1$s</string> + <string name="pref_title_notification_filter_reports">có một báo cáo mới</string> + <string name="notification_report_name">Báo cáo</string> + <string name="no_lists">Chưa có danh sách nào.</string> + <string name="report_category_violation">Vi phạm nội quy máy chủ</string> + <string name="report_category_spam">Spam</string> + <string name="report_category_other">Khác</string> + <string name="error_following_hashtags_unsupported">Máy chủ này không hỗ trợ theo dõi hashtag.</string> + <string name="title_followed_hashtags">Những hashtag theo dõi</string> + <string name="post_edited">Sửa %1$s</string> + <string name="description_post_edited">Đã sửa</string> + <string name="pref_default_post_language">Ngôn ngữ đăng mặc định (đồng bộ máy chủ)</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="error_muting_hashtag_format">Lỗi khi ẩn #%1$s</string> + <string name="error_unmuting_hashtag_format">Lỗi khi bỏ ẩn #%1$s</string> + <string name="hint_media_description_missing">Hình ảnh phải có mô tả.</string> + <string name="error_status_source_load">Không xác định được trạng thái máy chủ.</string> + <string name="pref_title_http_proxy_port_message">Cổng nên là khoảng giữa %1$d đến %2$d</string> + <string name="post_media_alt">✦</string> + <string name="action_discard">Hủy bỏ thay đổi</string> + <string name="action_continue_edit">Tiếp tục sửa</string> + <string name="compose_unsaved_changes">Thay đổi chưa được lưu.</string> + <string name="mute_notifications_switch">Ẩn thông báo</string> + <string name="status_edit_info">Sửa %1$s</string> + <string name="status_created_info">Đăng %1$s</string> + <string name="title_edits">Những lượt sửa tút</string> + <string name="a11y_label_loading_thread">Đang tải thảo luận</string> + <string name="action_share_account_link">Chia sẻ URL người dùng</string> + <string name="send_account_link_to">Chia sẻ URL người dùng…</string> + <string name="action_share_account_username">Chia sẻ tên người này</string> + <string name="send_account_username_to">Chia sẻ tên người này…</string> + <string name="account_username_copied">Đã sao chép tên người này</string> + <string name="pref_title_reading_order">Thứ tự đọc</string> + <string name="pref_reading_order_oldest_first">Cũ nhất trước</string> + <string name="pref_reading_order_newest_first">Mới nhất trước</string> + <string name="pref_summary_http_proxy_disabled">Tắt</string> + <string name="pref_summary_http_proxy_missing"><không đặt></string> + <string name="pref_summary_http_proxy_invalid"><không hợp lệ></string> + <string name="description_browser_login">Có thể hỗ trợ các phương thức xác thực bổ sung nhưng yêu cầu trình duyệt được hỗ trợ.</string> + <string name="action_browser_login">Đăng nhập bằng trình duyệt</string> + <string name="description_login">Đăng nhập ổn định. Dữ liệu sẽ không bị lộ.</string> + <string name="action_post_failed">Tải lên thất bại</string> + <string name="action_post_failed_detail">Tút của bạn không tải lên được và đã được lưu nháp. +\n +\nKhông thể liên lạc được với máy chủ hoặc nó đã từ chối tút.</string> + <string name="action_post_failed_detail_plural">Tút của bạn không tải lên được và đã được lưu nháp. +\n +\nKhông thể liên lạc được với máy chủ hoặc nó đã từ chối tút.</string> + <string name="action_post_failed_show_drafts">Xem bản nháp</string> + <string name="action_post_failed_do_nothing">Bỏ qua</string> + <string name="title_public_trending_hashtags">Hashtag xu hướng</string> + <string name="accessibility_talking_about_tag">%1$d người đang thảo luận về %2$s</string> + <string name="total_usage">lượt dùng</string> + <string name="total_accounts">người dùng</string> + <string name="dialog_follow_hashtag_title">Theo dõi hashtag</string> + <string name="dialog_follow_hashtag_hint">#hashtag</string> + <string name="action_refresh">Làm mới</string> + <string name="notification_unknown_name">Chưa biết</string> + <string name="socket_timeout_exception">Mất thời gian kết nối quá lâu</string> + <string name="ui_error_unknown">chưa rõ lý do</string> + <string name="ui_error_bookmark">Lưu tút không thành công: %1$s</string> + <string name="ui_error_clear_notifications">Xóa thông báo không thành công: %1$s</string> + <string name="ui_error_favourite">Thích tút không thành công: %1$s</string> + <string name="ui_error_reblog">Đăng lại tút không thành công: %1$s</string> + <string name="ui_error_vote">Bình chọn không thành công: %1$s</string> + <string name="ui_error_accept_follow_request">Chấp nhận theo dõi không thành công: %1$s</string> + <string name="ui_error_reject_follow_request">Từ chối theo dõi không thành công: %1$s</string> + <string name="ui_success_accepted_follow_request">Đã chấp nhận yêu cầu theo dõi</string> + <string name="ui_success_rejected_follow_request">Đã từ chối yêu cầu theo dõi</string> + <string name="status_filtered_show_anyway">Vẫn hiện</string> + <string name="status_filter_placeholder_label_format">Đã lọc: %1$s</string> + <string name="pref_title_account_filter_keywords">Người</string> + <string name="hint_filter_title">Bộ lọc của tôi</string> + <string name="label_filter_title">Tên bộ lọc</string> + <string name="filter_action_warn">Cảnh báo</string> + <string name="filter_action_hide">Đã ẩn</string> + <string name="filter_description_warn">Ẩn với cảnh báo</string> + <string name="filter_description_hide">Ẩn hoàn toàn</string> + <string name="label_filter_action">Hành động</string> + <string name="label_filter_context">Nơi áp dụng</string> + <string name="label_filter_keywords">Từ hoặc cụm từ muốn lọc</string> + <string name="action_add">Thêm</string> + <string name="filter_keyword_display_format">%1$s (cả từ)</string> + <string name="filter_keyword_addition_title">Thêm từ</string> + <string name="filter_edit_keyword_title">Sửa từ</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="pref_title_show_stat_inline">Hiện số tương tác trên tút</string> + <string name="help_empty_home">Đây là <b>bảng tin của bạn</b>. Nó sẽ hiện tút gần đây từ những người bạn theo dõi. +\n +\nĐể khám phá mọi người, bạn có thể xem qua ở các bảng tin khác. Ví dụ: [iconics gmd_group] Bảng tin máy chủ của bạn. Hoặc bạn cũng có thể [iconics gmd_search] tìm theo tên người dùng; ví dụ như Tusky.</string> + <string name="post_media_image">Hình ảnh</string> + <string name="select_list_manage">Quản lý danh sách</string> + <string name="load_newest_notifications">Tải những thông báo mới nhất</string> + <string name="compose_delete_draft">Xóa bản nháp\?</string> + <string name="error_missing_edits">Máy chủ của bạn biết tút đã được sửa, nhưng không có bản sao của lần sửa, nên nó không thể hiện. +\n +\nĐây là <a href="https://github.com/mastodon/mastodon/issues/25398">Lỗi Mastodon #25398</a>.</string> + <string name="pref_ui_text_size">Cỡ chữ giao diện</string> + <string name="notification_listenable_worker_name">Hoạt động chạy nền</string> + <string name="notification_listenable_worker_description">Thông báo khi Tusky hoạt động ngầm</string> + <string name="notification_notification_worker">Đang nạp thông báo…</string> + <string name="notification_prune_cache">Bảo trì bộ nhớ đệm…</string> + <string name="error_media_upload_sending_fmt">Không thể tải lên: %1$s</string> + <string name="about_device_info">%1$s %2$s +\nAndroid %3$s +\nSDK %4$d</string> + <string name="about_account_info_title">Tài khoản của bạn</string> + <string name="about_account_info">\@%1$s@%2$s +\nPhiên bản: %3$s</string> + <string name="about_copy">Sao chép phiên bản và thông tin thiết bị</string> + <string name="about_copied">Đã sao chép phiên bản và thông tin thiết bị</string> + <string name="about_device_info_title">Thiết bị của bạn</string> + <string name="list_exclusive_label">Ẩn khỏi bảng tin</string> + <string name="error_media_playback">Không thể phát: %1$s</string> + <string name="dialog_delete_filter_text">Xóa bộ lọc \'%1$s\'\?</string> + <string name="dialog_delete_filter_positive_action">Xóa</string> + <string name="dialog_save_profile_changes_message">Bạn có chắc muốn lưu thay đổi\?</string> + <string name="help_empty_conversations">Đây là <b>tin nhắn riêng</b>; đôi khi gọi là thảo luận hoặc nhắn riêng (DM). +\n +\nTin nhắn riêng được tạo bằng cách chọn tùy chọn kiểu tút [iconics gmd_public] thành [iconics gmd_mail] <i>Nhắn riêng</i> và có nhắc đến một người nào đó. +\n +\nVí dụ: bạn có thể xem hồ sơ của một người và nhấn vào nút [iconics gmd_edit] và đổi kiểu tút. </string> + <string name="help_empty_lists">Đây là <b>danh sách</b>. Bạn có thể tạo nhiều danh sách riêng và thêm người dùng vào đó. +\n +\nBạn chỉ có thể thêm những người MÀ BẠN THEO DÕI vào danh sách. +\n +\nDanh sách có thể được sử dụng như một tab trong thiết lập Cá nhân [iconics gmd_account_circle] [iconics gmd_navigate_next] Xếp tab. </string> + <string name="muting_hashtag_success_format">Đang ẩn hashtag #%1$s như một cảnh báo</string> + <string name="unmuting_hashtag_success_format">Đang bỏ ẩn hashtag #%1$s</string> + <string name="action_view_filter">Xem bộ lọc</string> + <string name="following_hashtag_success_format">Đã theo dõi hashtag #%1$s</string> + <string name="unfollowing_hashtag_success_format">Đã bỏ theo dõi hashtag #%1$s</string> + <string name="error_blocking_domain">Không thể ẩn %1$s: %2$s</string> + <string name="error_unblocking_domain">Không thể bỏ ẩn %1$s: %2$s</string> + <string name="label_image">Hình ảnh</string> + <string name="app_theme_system_black">Mặc định của thiết bị (Đen)</string> + <string name="title_public_trending_statuses">Tút xu hướng</string> + <string name="list_reply_policy_none">Không ai</string> + <string name="list_reply_policy_list">Người trong danh sách</string> + <string name="list_reply_policy_followed">Người đã theo dõi</string> + <string name="list_reply_policy_label">Hiện lượt trả lời</string> + <string name="pref_title_show_self_boosts">Hiện lượt tự đăng lại</string> + <string name="pref_title_show_self_boosts_description">Ai đó đăng lại tút của chính họ</string> + <string name="pref_title_per_timeline_preferences">Thiết lập từng bảng tin</string> + <string name="pref_title_show_notifications_filter">Hiện bộ lọc thông báo</string> + <string name="reply_sending">Đang gửi…</string> + <string name="reply_sending_long">Câu trả lời của bạn đã được gửi đi.</string> + <string name="action_translate">Dịch</string> + <string name="action_show_original">Xem bản chưa dịch</string> + <string name="label_translating">Đang dịch…</string> + <string name="label_translated">Dịch %1$s bằng %2$s</string> + <string name="ui_error_translate">Không thể dịch: %1$s</string> + <string name="unknown_notification_type">Kiểu thông báo chưa rõ</string> + <string name="report_category_legal">Pháp luật</string> + <string name="dialog_follow_warning">Theo dõi người này?</string> + <string name="pref_title_confirm_follows">Hỏi trước khi theo dõi</string> + <string name="url_copied">Đã sao chép liên kết</string> + <string name="confirmation_hashtag_copied">Đã sao chép \'#%1$s\'</string> + <string name="pref_default_reply_privacy">Kiểu trả lời mặc định (không đồng bộ máy chủ)</string> + <string name="error_deleting_filter">Xóa bộ lọc \'%1$s\' không được</string> + <string name="error_saving_filter">Chưa thể lưu bộ lọc \'%1$s\'</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-w640dp/dimens.xml b/app/src/main/res/values-w640dp/dimens.xml new file mode 100644 index 0000000..41b374b --- /dev/null +++ b/app/src/main/res/values-w640dp/dimens.xml @@ -0,0 +1,3 @@ +<resources> + <dimen name="timeline_width">640dp</dimen> +</resources> diff --git a/app/src/main/res/values-w640dp/integers.xml b/app/src/main/res/values-w640dp/integers.xml new file mode 100644 index 0000000..ad79f91 --- /dev/null +++ b/app/src/main/res/values-w640dp/integers.xml @@ -0,0 +1,4 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <integer name="trending_column_count">3</integer> +</resources> diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml new file mode 100644 index 0000000..71d0c8e --- /dev/null +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -0,0 +1,701 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">出错了。</string> + <string name="error_network">网络请求出错,请检查互联网连接并重试。</string> + <string name="error_empty">内容不能为空。</string> + <string name="error_invalid_domain">该域名无效</string> + <string name="error_failed_app_registration">未能通过该实例的身份验证。如果这个问题持续,请从菜单处尝试“在浏览器中登录”。</string> + <string name="error_no_web_browser_found">找不到可用的浏览器。</string> + <string name="error_authorization_unknown">发生不明授权错误。如果这个问题持续,请从菜单处尝试 “在浏览器中登录”。</string> + <string name="error_authorization_denied">授权被拒绝。如果你确定提供了正确的凭据,请从菜单处尝试“在浏览器中登录”。</string> + <string name="error_retrieving_oauth_token">未能获取登录令牌。如果这个问题持续,请从菜单处尝试 “在浏览器中登录”。</string> + <string name="error_compose_character_limit">嘟文太长了!</string> + <string name="error_media_upload_type">无法上传此类型的文件。</string> + <string name="error_media_upload_opening">此文件无法打开。</string> + <string name="error_media_upload_permission">需要授予 Tusky 读取媒体文件的权限。</string> + <string name="error_media_download_permission">需要授予 Tusky 存储媒体的权限。</string> + <string name="error_media_upload_image_or_video">无法在一篇嘟文中同时插入视频和图片。</string> + <string name="error_media_upload_sending">上传失败。</string> + <string name="error_sender_account_gone">嘟文发送时出错。</string> + <string name="title_home">主页</string> + <string name="title_notifications">通知</string> + <string name="title_public_local">本站时间轴</string> + <string name="title_public_federated">跨站时间轴</string> + <string name="title_direct_messages">私信</string> + <string name="title_tab_preferences">标签页</string> + <string name="title_view_thread">长嘟文</string> + <string name="title_posts">嘟文</string> + <string name="title_posts_with_replies">嘟文和回复</string> + <string name="title_posts_pinned">已置顶</string> + <string name="title_follows">正在关注</string> + <string name="title_followers">关注者</string> + <string name="title_favourites">收藏</string> + <string name="title_mutes">被隐藏的用户</string> + <string name="title_blocks">被屏蔽的用户</string> + <string name="title_follow_requests">关注请求</string> + <string name="title_edit_profile">编辑个人资料</string> + <string name="title_drafts">草稿</string> + <string name="title_licenses">开源协议</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s 转嘟了</string> + <string name="post_sensitive_media_title">敏感内容</string> + <string name="post_media_hidden_title">已隐藏的照片或视频</string> + <string name="post_sensitive_media_directions">点击查看</string> + <string name="post_content_warning_show_more">显示更多</string> + <string name="post_content_warning_show_less">折叠内容</string> + <string name="post_content_show_more">展开</string> + <string name="post_content_show_less">折叠</string> + <string name="message_empty">还没有内容。</string> + <string name="footer_empty">还没有内容,向下拉动即可刷新!</string> + <string name="notification_reblog_format">%1$s 转嘟了你的嘟文</string> + <string name="notification_favourite_format">%1$s 喜欢了你的嘟文</string> + <string name="notification_follow_format">%1$s 关注了你</string> + <string name="report_username_format">举报 @%1$s</string> + <string name="report_comment_hint">报告更多信息?</string> + <string name="action_quick_reply">快速回复</string> + <string name="action_reply">回复</string> + <string name="action_reblog">转嘟</string> + <string name="action_unreblog">取消转嘟</string> + <string name="action_favourite">喜欢</string> + <string name="action_unfavourite">取消喜欢</string> + <string name="action_more">更多</string> + <string name="action_compose">发表嘟文</string> + <string name="action_login">用 Tusky 登录</string> + <string name="action_logout">注销</string> + <string name="action_logout_confirm">你确定要退出登录 %1$s 吗?这会删除账户的所有本地数据,包括草稿和选项。</string> + <string name="action_follow">关注</string> + <string name="action_unfollow">取消关注</string> + <string name="action_block">屏蔽</string> + <string name="action_unblock">取消屏蔽</string> + <string name="action_hide_reblogs">隐藏转嘟</string> + <string name="action_show_reblogs">显示转嘟</string> + <string name="action_report">举报</string> + <string name="action_delete">删除</string> + <string name="action_delete_and_redraft">删除并重新编辑</string> + <string name="action_send">嘟嘟</string> + <string name="action_send_public">嘟嘟!</string> + <string name="action_retry">重试</string> + <string name="action_close">关闭</string> + <string name="action_view_profile">个人资料</string> + <string name="action_view_preferences">设置</string> + <string name="action_view_account_preferences">账户设置</string> + <string name="action_view_favourites">喜欢</string> + <string name="action_view_mutes">被隐藏的用户</string> + <string name="action_view_blocks">被屏蔽的用户</string> + <string name="action_view_follow_requests">关注请求</string> + <string name="action_view_media">媒体</string> + <string name="action_open_in_web">在浏览器中打开</string> + <string name="action_add_media">添加媒体</string> + <string name="action_photo_take">拍照</string> + <string name="action_share">分享</string> + <string name="action_mute">隐藏</string> + <string name="action_unmute">取消隐藏</string> + <string name="action_mention">提及</string> + <string name="action_hide_media">隐藏媒体文件</string> + <string name="action_open_drawer">打开菜单</string> + <string name="action_save">保存</string> + <string name="action_edit_profile">编辑个人资料</string> + <string name="action_edit_own_profile">编辑</string> + <string name="action_undo">撤销</string> + <string name="action_accept">接受</string> + <string name="action_reject">拒绝</string> + <string name="action_search">搜索</string> + <string name="action_access_drafts">草稿</string> + <string name="action_toggle_visibility">设置嘟文可见范围</string> + <string name="action_content_warning">设置内容提醒</string> + <string name="action_emoji_keyboard">插入表情符号</string> + <string name="action_add_tab">添加标签页</string> + <string name="action_links">链接</string> + <string name="action_mentions">提及</string> + <string name="action_hashtags">话题</string> + <string name="action_open_reblogger">打开转嘟用户主页</string> + <string name="action_open_reblogged_by">显示转嘟</string> + <string name="action_open_faved_by">显示喜欢</string> + <string name="title_hashtags_dialog">话题</string> + <string name="title_mentions_dialog">提及</string> + <string name="title_links_dialog">链接</string> + <string name="action_open_media_n">打开媒体文件 #%1$d</string> + <string name="download_image">下载中 %1$s</string> + <string name="action_copy_link">复制链接</string> + <string name="action_open_as">打开为 %1$s</string> + <string name="action_share_as">分享为 …</string> + <string name="download_media">下载媒体文件</string> + <string name="downloading_media">正在下载媒体文件</string> + <string name="send_post_link_to">分享嘟文链接到…</string> + <string name="send_post_content_to">分享嘟文到…</string> + <string name="send_media_to">分享媒体到…</string> + <string name="confirmation_reported">已发送!</string> + <string name="confirmation_unblocked">已解除屏蔽</string> + <string name="confirmation_unmuted">已取消隐藏</string> + <string name="hint_domain">哪个实例?</string> + <string name="hint_compose">有什么新鲜事?</string> + <string name="hint_content_warning">内容提醒</string> + <string name="hint_display_name">昵称</string> + <string name="hint_note">简介</string> + <string name="hint_search">搜索…</string> + <string name="search_no_results">没找到结果</string> + <string name="label_quick_reply">回复…</string> + <string name="label_avatar">头像</string> + <string name="label_header">标题</string> + <string name="link_whats_an_instance">什么是实例?</string> + <string name="login_connection">正在连接…</string> + <string name="dialog_whats_an_instance">请输入你账号所在的 Mastodon 站点的域名,比如 mastodon.social,icosahedron.website,social.tchncs.de,<a href="https://instances.social">等等</a> 。 +\n +\n还没有 Mastodon 账号?你也可以输入想注册的 Mastodon 站点的域名,然后在该服务器创建新的账号并授权 Tusky 登入。 +\n +\n在 Mastodon 里,你的账号信息储存在某一特定实例当中,但 Mastodon 可使跨站互动和站内互动一样简单。 +\n +\n可以前往 <a href="https://joinmastodon.org">https://joinmastodon.org</a> 了解更多信息。 </string> + <string name="dialog_title_finishing_media_upload">正在结束上传</string> + <string name="dialog_message_uploading_media">正在上传…</string> + <string name="dialog_download_image">下载</string> + <string name="dialog_message_cancel_follow_request">移除关注请求?</string> + <string name="dialog_unfollow_warning">不再关注此用户?</string> + <string name="dialog_delete_post_warning">删除这条嘟文?</string> + <string name="dialog_redraft_post_warning">删除并重新起草这条嘟文?</string> + <string name="visibility_public">公开:所有人可见,并会出现在公共时间轴上</string> + <string name="visibility_unlisted">不公开:所有人可见,但不会出现在公共时间轴上</string> + <string name="visibility_private">仅关注者:只有经过你确认后关注你的用户可见</string> + <string name="visibility_direct">私信:只有被提及的用户可见</string> + <string name="pref_title_edit_notification_settings">通知</string> + <string name="pref_title_notifications_enabled">通知</string> + <string name="pref_title_notification_alerts">提醒</string> + <string name="pref_title_notification_alert_sound">铃声</string> + <string name="pref_title_notification_alert_vibrate">振动</string> + <string name="pref_title_notification_alert_light">呼吸灯</string> + <string name="pref_title_notification_filters">事件</string> + <string name="pref_title_notification_filter_mentions">被提及</string> + <string name="pref_title_notification_filter_follows">有新的关注者</string> + <string name="pref_title_notification_filter_reblogs">嘟文被转嘟</string> + <string name="pref_title_notification_filter_favourites">嘟文被喜欢</string> + <string name="pref_title_notification_filter_poll">投票已结束</string> + <string name="pref_title_appearance_settings">外观</string> + <string name="pref_title_app_theme">应用主题</string> + <string name="pref_title_timelines">时间轴</string> + <string name="pref_title_timeline_filters">过滤器</string> + <string name="app_them_dark">暗色</string> + <string name="app_theme_light">亮色</string> + <string name="app_theme_black">黑色</string> + <string name="app_theme_auto">自动切换</string> + <string name="app_theme_system">跟随系统设定</string> + <string name="pref_title_browser_settings">浏览器</string> + <string name="pref_title_custom_tabs">使用 Chrome Custom Tabs</string> + <string name="pref_title_language">界面语言</string> + <string name="pref_title_post_filter">时间轴过滤</string> + <string name="pref_title_post_tabs">标签页</string> + <string name="pref_title_show_boosts">显示转嘟</string> + <string name="pref_title_show_replies">显示回复</string> + <string name="pref_title_show_media_preview">显示预览图</string> + <string name="pref_title_proxy_settings">代理</string> + <string name="pref_title_http_proxy_settings">HTTP 代理</string> + <string name="pref_title_http_proxy_enable">启用 HTTP 代理</string> + <string name="pref_title_http_proxy_server">HTTP 代理服务器</string> + <string name="pref_title_http_proxy_port">HTTP 代理端口</string> + <string name="pref_default_post_privacy">嘟文默认可见范围</string> + <string name="pref_default_media_sensitivity">自动标记媒体为敏感内容</string> + <string name="pref_publishing">发布(与服务器同步)</string> + <string name="pref_failed_to_sync">同步选项失败</string> + <string name="post_privacy_public">公开</string> + <string name="post_privacy_unlisted">不公开</string> + <string name="post_privacy_followers_only">仅关注者</string> + <string name="pref_post_text_size">嘟文字体大小</string> + <string name="post_text_size_smallest">最小</string> + <string name="post_text_size_small">小</string> + <string name="post_text_size_medium">标准</string> + <string name="post_text_size_large">大</string> + <string name="post_text_size_largest">最大</string> + <string name="notification_mention_name">新提及</string> + <string name="notification_mention_descriptions">当有用户在嘟文中提及我时</string> + <string name="notification_follow_name">新关注者</string> + <string name="notification_follow_description">当有用户关注我时</string> + <string name="notification_boost_name">转嘟</string> + <string name="notification_boost_description">当我的嘟文被转发时通知</string> + <string name="notification_favourite_name">喜欢</string> + <string name="notification_favourite_description">当有用户喜欢了我的嘟文时</string> + <string name="notification_poll_name">投票</string> + <string name="notification_poll_description">当我参与的投票结束时</string> + <string name="notification_mention_format">%1$s 提及了你</string> + <string name="notification_summary_large">%1$s,%2$s,%3$s 和 %4$d 等人</string> + <string name="notification_summary_medium">%1$s,%2$s 和 %3$s</string> + <string name="notification_summary_small">%1$s 和 %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="other">%1$d 个新互动</item> + </plurals> + <string name="description_account_locked">锁嘟用户</string> + <string name="about_title_activity">关于 Tusky</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tusky 是基于 GNU General Public License Version 3 许可证开源的自由软件。完整的许可证协议:https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">项目地址:https://tusky.app</string> + <string name="about_bug_feature_request_site">问题反馈及功能请求: +\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Tusky 官方账号</string> + <string name="post_share_content">分享嘟文内容</string> + <string name="post_share_link">分享嘟文链接</string> + <string name="post_media_images">图片</string> + <string name="post_media_video">视频</string> + <string name="state_follow_requested">已发送关注请求</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">%1$d 年内</string> + <string name="abbreviated_in_days">%1$d 天内</string> + <string name="abbreviated_in_hours">%1$d 小时内</string> + <string name="abbreviated_in_minutes">%1$d 分钟内</string> + <string name="abbreviated_in_seconds">%1$d 秒内</string> + <string name="abbreviated_years_ago">%1$d 年前</string> + <string name="abbreviated_days_ago">%1$d 天前</string> + <string name="abbreviated_hours_ago">%1$d 小时前</string> + <string name="abbreviated_minutes_ago">%1$d 分钟前</string> + <string name="abbreviated_seconds_ago">%1$d 秒前</string> + <string name="follows_you">关注了你</string> + <string name="pref_title_alway_show_sensitive_media">总是显示所有敏感媒体内容</string> + <string name="title_media">媒体</string> + <string name="replying_to">回复 @%1$s</string> + <string name="load_more_placeholder_text">加载更多</string> + <string name="pref_title_public_filter_keywords">公共时间轴</string> + <string name="pref_title_thread_filter_keywords">对话</string> + <string name="filter_addition_title">添加新的过滤器</string> + <string name="filter_edit_title">编辑过滤器</string> + <string name="filter_dialog_remove_button">移除</string> + <string name="filter_dialog_update_button">更新</string> + <string name="filter_add_description">需要过滤的文字</string> + <string name="add_account_name">添加账号</string> + <string name="add_account_description">添加新的 Mastodon 账号</string> + <string name="action_lists">列表</string> + <string name="title_lists">列表</string> + <string name="error_create_list">无法新建列表</string> + <string name="error_rename_list">无法更新列表</string> + <string name="error_delete_list">无法删除列表</string> + <string name="action_create_list">新建列表</string> + <string name="action_rename_list">更新列表</string> + <string name="action_delete_list">删除列表</string> + <string name="hint_search_people_list">搜索已关注的用户</string> + <string name="action_add_to_list">添加用户到列表</string> + <string name="action_remove_from_list">从列表中移除用户</string> + <string name="compose_active_account_description">以 %1$s 身份发布嘟文</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">为视觉障碍用户描述内容(最多 %1$d 个字符)</item> + </plurals> + <string name="action_set_caption">设置图片标题</string> + <string name="action_remove">移除</string> + <string name="lock_account_label">保护你的账户(锁嘟)</string> + <string name="lock_account_label_description">你需要手动审核所有关注请求</string> + <string name="compose_save_draft">保存为草稿?</string> + <string name="send_post_notification_title">正在发送嘟文…</string> + <string name="send_post_notification_error_title">嘟文发送出错</string> + <string name="send_post_notification_channel_name">嘟文发送中</string> + <string name="send_post_notification_cancel_title">已取消发送</string> + <string name="send_post_notification_saved_content">嘟文副本已保存为草稿</string> + <string name="action_compose_shortcut">发表嘟文</string> + <string name="error_no_custom_emojis">当前实例 %1$s 没有自定义表情符号</string> + <string name="emoji_style">表情符号风格</string> + <string name="system_default">系统默认</string> + <string name="download_fonts">需要下载表情符号数据</string> + <string name="performing_lookup_title">正在查询…</string> + <string name="expand_collapse_all_posts">展开/折叠所有嘟文</string> + <string name="action_open_post">打开嘟文</string> + <string name="restart_required">需要重启应用</string> + <string name="restart_emoji">你需要重启 Tusky 才能生效</string> + <string name="later">稍后</string> + <string name="restart">立即重启</string> + <string name="caption_systememoji">系统内置的表情符号</string> + <string name="caption_blobmoji">Android 4.4–7.1 的黄馒头表情符号</string> + <string name="caption_twemoji">Mastodon 使用的表情符号</string> + <!-- string name="emoji_shortcode_format" translatable="false">:%1$s:</string --> + <string name="download_failed">下载失败</string> + <string name="profile_badge_bot_text">机器人</string> + <string name="account_moved_description">%1$s 已迁移到:</string> + <string name="reblog_private">转嘟(可见者不变)</string> + <string name="unreblog_private">取消转嘟</string> + <string name="license_description">Tusky 使用了以下开源项目的源码:</string> + <string name="license_apache_2">以 Apache License 授权(详见下方)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">个人资料附加信息</string> + <string name="profile_metadata_add">添加信息</string> + <string name="profile_metadata_label_label">标签</string> + <string name="profile_metadata_content_label">内容</string> + <string name="pref_title_absolute_time">嘟文显示精确时间</string> + <string name="label_remote_account">以下信息可能并不完整,要查看完整资料请使用浏览器打开。</string> + <string name="unpin_action">取消置顶</string> + <string name="pin_action">置顶</string> + <plurals name="favs"> + <item quantity="other"><b>%1$s</b> 次喜欢</item> + </plurals> + <plurals name="reblogs"> + <item quantity="other"><b>%1$s</b> 次转嘟</item> + </plurals> + <string name="title_reblogged_by">转嘟</string> + <string name="title_favourited_by">喜欢</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s 和 %2$s</string> + <string name="conversation_more_recipients">%1$s,%2$s 和 %3$d 等人</string> + <string name="description_post_media">媒体:%1$s</string> + <string name="description_post_cw">内容警告:%1$s</string> + <string name="description_post_media_no_description_placeholder">没有描述信息</string> + <string name="description_post_reblogged">被转嘟</string> + <string name="description_post_favourited">被喜欢</string> + <string name="description_visibility_public"> + 公开 + </string> + <string name="description_visibility_unlisted"> + 不公开 + </string> + <string name="description_visibility_private">仅关注者</string> + <string name="description_visibility_direct"> + 私信 + </string> + <string name="hint_list_name">列表名</string> + <string name="edit_hashtag_hint">话题名(不含前面的 # 号)</string> + <string name="notifications_clear">删除</string> + <string name="notifications_apply_filter">筛选</string> + <string name="filter_apply">应用</string> + <string name="compose_shortcut_long_label">撰写嘟文</string> + <string name="compose_shortcut_short_label">发表嘟文</string> + <string name="pref_title_bot_overlay">显示机器人标志</string> + <string name="notification_clear_text">你确定要永久清空通知列表吗?</string> + <string name="poll_info_format"> <!-- 15 票 • 1 小时剩余 --> %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="other">%1$s 次投票</item> + </plurals> + <string name="poll_info_time_absolute">%1$s 结束</string> + <string name="poll_info_closed">已结束</string> + <string name="poll_vote">投票</string> + <string name="poll_ended_voted">你参与的投票已结束</string> + <string name="poll_ended_created">你创建的投票已结束</string> + <!--These are for timestamps on polls --> + <plurals name="poll_timespan_days"> + <item quantity="other">剩余 %1$d 天</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="other">剩余 %1$d 小时</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="other">剩余 %1$d 分钟</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="other">剩余 %1$d 秒</item> + </plurals> + <string name="action_reset_schedule">重置</string> + <string name="title_bookmarks">书签</string> + <string name="title_domain_mutes">被隐藏的域名</string> + <string name="title_scheduled_posts">定时嘟文</string> + <string name="action_bookmark">书签</string> + <string name="action_edit">编辑</string> + <string name="action_view_bookmarks">书签</string> + <string name="action_view_domain_mutes">隐藏的域名</string> + <string name="action_add_poll">新增投票</string> + <string name="action_access_scheduled_posts">定时嘟文</string> + <string name="action_schedule_post">定时嘟文</string> + <string name="confirmation_domain_unmuted">%1$s 已取消隐藏</string> + <string name="mute_domain_warning_dialog_ok">隐藏来自该域名的所有嘟文</string> + <string name="pref_title_animate_gif_avatars">动画GIF头像</string> + <string name="about_powered_by_tusky">由Tusky提供支持</string> + <string name="pref_title_alway_open_spoiler">始终扩展有内容警告的嘟文</string> + <string name="filter_dialog_whole_word">整个单词</string> + <string name="filter_dialog_whole_word_description">如果关键字或缩写只有字母或数字,则只有在匹配整个单词时才会应用</string> + <string name="caption_notoemoji">Google正在使用的表情符号</string> + <string name="description_post_bookmarked">被加入书签</string> + <string name="description_poll">使用以下选项创建投票:%1$s,%2$s,%3$s,%4$s;%5$s</string> + <string name="select_list_title">选择列表</string> + <string name="list">列表</string> + <string name="button_continue">继续</string> + <string name="button_back">返回</string> + <string name="button_done">完成</string> + <string name="report_sent_success">成功举报 @%1$s</string> + <string name="hint_additional_info">附加留言</string> + <string name="report_remote_instance">转发到 %1$s</string> + <string name="failed_report">举报失败</string> + <string name="failed_fetch_posts">无法获取嘟文</string> + <string name="report_description_1">举报将发送给你所在服务器的管理员。你可以在下面提供举报此账户的相关说明:</string> + <string name="report_description_remote_instance">该账户来自其他服务器。向那里发送一份匿名的报告副本?</string> + <string name="title_accounts">账户</string> + <string name="failed_search">搜索失败</string> + <string name="create_poll_title">投票</string> + <string name="duration_5_min">5 分钟</string> + <string name="duration_30_min">30 分钟</string> + <string name="duration_1_hour">1 小时</string> + <string name="duration_6_hours">6 小时</string> + <string name="duration_1_day">1 天</string> + <string name="duration_3_days">3 天</string> + <string name="duration_7_days">7 天</string> + <string name="add_poll_choice">添加选择</string> + <string name="poll_allow_multiple_choices">多项选择</string> + <string name="poll_new_choice_hint">选择 %1$d</string> + <string name="edit_poll">编辑</string> + <string name="post_lookup_error_format">查找嘟文时出错 %1$s</string> + <string name="no_drafts">你没有草稿。</string> + <string name="no_scheduled_posts">您没有任何定时嘟文。</string> + <string name="warning_scheduling_interval">Mastodon的最小预订时间为5分钟。</string> + <string name="notification_follow_request_name">关注请求</string> + <string name="hashtags">话题</string> + <string name="pref_title_confirm_reblogs">转嘟前提示确认</string> + <string name="pref_title_show_cards_in_timelines">在时间轴上显示链接预览</string> + <string name="pref_title_enable_swipe_for_tabs">滑动切换标签页</string> + <string name="compose_preview_image_description">处理图片 %1$s</string> + <string name="add_hashtag_title">添加话题名</string> + <string name="notification_follow_request_description">关注请求的通知</string> + <string name="pref_main_nav_position_option_bottom">底部</string> + <string name="pref_main_nav_position_option_top">顶部</string> + <string name="pref_title_gradient_for_media">为隐藏的媒体文件显示彩色模糊预览</string> + <string name="pref_title_notification_filter_follow_requests">有关注申请</string> + <string name="dialog_mute_hide_notifications">隐藏通知</string> + <string name="dialog_mute_warning">确定隐藏 @%1$s?</string> + <string name="dialog_block_warning">确定屏蔽 @%1$s?</string> + <string name="mute_domain_warning">确定要完全屏蔽 %1$s 吗?您将不能在公共时间轴和通知内看见来自此域名的内容,且您在此域名上的关注者将会被移除。</string> + <string name="action_unmute_conversation">取消隐藏会话</string> + <string name="action_mute_conversation">隐藏会话</string> + <string name="action_unmute_domain">取消隐藏 %1$s</string> + <string name="action_mute_domain">隐藏 %1$s</string> + <string name="action_unmute_desc">取消隐藏 %1$s</string> + <string name="notification_follow_request_format">%1$s 请求关注你</string> + <string name="pref_main_nav_position">导航栏位置</string> + <string name="pref_title_hide_top_toolbar">隐藏顶部工具栏标题</string> + <string name="no_announcements">本站暂无公告。</string> + <string name="title_announcements">公告</string> + <string name="account_note_saved">已保存!</string> + <string name="account_note_hint">此账号的备注</string> + <string name="action_unsubscribe_account">取消关注</string> + <string name="action_subscribe_account">关注</string> + <string name="drafts_post_reply_removed">该草稿回复的原嘟文已被删除</string> + <string name="draft_deleted">草稿已删除</string> + <string name="drafts_failed_loading_reply">加载回复信息失败</string> + <string name="drafts_post_failed_to_send">嘟文发送失败!</string> + <string name="dialog_delete_list_warning">确认删除列表 %1$s?</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="other">最多只可上传 %1$d 个媒体附件。</item> + </plurals> + <string name="wellbeing_hide_stats_profile">隐藏账号的统计信息</string> + <string name="review_notifications">反馈通知</string> + <string name="wellbeing_hide_stats_posts">隐藏嘟文的统计信息</string> + <string name="limit_notifications">限制时间线通知</string> + <string name="wellbeing_mode_notice">一些可能影响你精神状态的信息将被隐藏,包括: +\n +\n - 喜欢、转发、关注通知 +\n - 嘟文的喜欢、转发数 +\n - 账号的已关注数量、嘟文数量 +\n +\n 推送通知不会被影响,但可以在通知设置中手动禁用。</string> + <string name="pref_title_wellbeing_mode">健康模式</string> + <string name="duration_indefinite">永久</string> + <string name="label_duration">持续时间</string> + <plurals name="poll_info_people"> + <item quantity="other">%1$s 人</item> + </plurals> + <string name="post_media_attachments">附件</string> + <string name="post_media_audio">音频</string> + <string name="notification_subscription_description">当我关注的用户发布了新嘟文时通知</string> + <string name="notification_subscription_name">新嘟文</string> + <string name="pref_title_animate_custom_emojis">显示动态自定义Emoji</string> + <string name="pref_title_notification_filter_subscriptions">关注的人发布了新嘟文</string> + <string name="notification_subscription_format">%1$s 刚刚发送了新嘟文</string> + <string name="follow_requests_info">即使你的账号未上锁,但 %1$s 的管理员认为你可能需要手动处理这些账号的关注请求。</string> + <string name="dialog_delete_conversation_warning">删除此对话吗?</string> + <string name="action_delete_conversation">删除对话</string> + <string name="pref_title_confirm_favourites">收藏前提示确认</string> + <string name="action_unbookmark">删除书签</string> + <string name="duration_30_days">30 天</string> + <string name="duration_60_days">60 天</string> + <string name="duration_90_days">90 天</string> + <string name="duration_180_days">180 天</string> + <string name="duration_14_days">14 天</string> + <string name="duration_365_days">365 天</string> + <string name="tusky_compose_post_quicksetting_label">撰写嘟文</string> + <string name="notification_sign_up_format">%1$s 已注册</string> + <string name="pref_title_notification_filter_sign_ups">某人进行了注册</string> + <string name="notification_sign_up_description">新用户通知</string> + <string name="notification_sign_up_name">注册</string> + <string name="title_login">登录</string> + <string name="notification_update_format">%1$s 编辑了他们的嘟文</string> + <string name="pref_title_notification_filter_updates">我进行过互动的嘟文被编辑了</string> + <string name="notification_update_name">嘟文编辑</string> + <string name="notification_update_description">当你进行过互动的嘟文被编辑时发出通知</string> + <string name="error_could_not_load_login_page">无法加载登录页。</string> + <string name="saving_draft">正在保存草稿…</string> + <string name="title_migration_relogin">重新登陆以启用通知推送</string> + <string name="action_dismiss">不理会</string> + <string name="action_details">详情</string> + <string name="dialog_push_notification_migration_other_accounts">你已重新登录当前账户,向 Tusky 授予推送订阅权限。但是,你仍然有其他没有以这种方式迁移的账户。切换到它们,逐个重新登录,以启用 UnifiedPush 通知支持。</string> + <string name="account_date_joined">加入于%1$s</string> + <string name="tips_push_notification_migration">重新登录所有账户来启用推送通知支持。</string> + <string name="dialog_push_notification_migration">为了通过 UnifiedPush 使用推送通知,Tusky 需要订阅你 Mastodon 服务器通知的权限。这需要重新登录来更改授予 Tusky 的 OAuth 作用域。使用此处或账户首选项中“重新登录”选项将保留你所有的本地草稿和缓存。</string> + <string name="status_count_one_plus">1+</string> + <string name="action_edit_image">编辑图片</string> + <string name="error_image_edit_failed">无法编辑图片。</string> + <string name="error_loading_account_details">加载账户详情失败</string> + <string name="error_multimedia_size_limit">音视频文件大小不能超出 %1$s MB。</string> + <string name="error_following_hashtag_format">关注 #%1$s 出错</string> + <string name="error_unfollowing_hashtag_format">取关 #%1$s 出错</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="duration_no_change">(无更改)</string> + <string name="description_post_language">嘟文语言</string> + <string name="action_set_focus">设置焦点</string> + <string name="set_focus_description">轻按或拖动圆圈选择始终在缩略图中可见的焦点。</string> + <string name="pref_show_self_username_disambiguate">登录多个账户时</string> + <string name="pref_title_show_self_username">在工具栏中显示用户名</string> + <string name="pref_show_self_username_always">始终</string> + <string name="pref_show_self_username_never">从不</string> + <string name="delete_scheduled_post_warning">删除这条定时嘟文吗?</string> + <string name="instance_rule_info">登录即表示您同意 %1$s 的规定。</string> + <string name="instance_rule_title">%1$s 的规定</string> + <string name="compose_save_draft_loses_media">保存草稿?(恢复草稿时附件将被再次上传)</string> + <string name="failed_to_pin">固定失败</string> + <string name="failed_to_unpin">取消固定失败</string> + <string name="action_add_reaction">添加回应</string> + <string name="notification_report_format">%1$s 的新报告</string> + <string name="notification_header_report_format">%1$s 报告了 %2$s</string> + <string name="pref_title_notification_filter_reports">有新报告</string> + <string name="notification_report_name">报告</string> + <string name="action_add_or_remove_from_list">从列表中添加或删除</string> + <string name="failed_to_add_to_list">未能将账户添加到列表</string> + <string name="failed_to_remove_from_list">未能从列表中删除账户</string> + <string name="no_lists">你没有任何列表。</string> + <string name="action_unfollow_hashtag_format">取关 #%1$s 吗?</string> + <string name="status_created_at_now">立即</string> + <string name="notification_summary_report_format">附上了 %1$s · %2$d 个嘟文</string> + <string name="confirmation_hashtag_unfollowed">取关了 #%1$s</string> + <string name="notification_report_description">审核报告的通知</string> + <string name="report_category_violation">违反规则</string> + <string name="report_category_spam">垃圾信息</string> + <string name="report_category_other">其他</string> + <string name="error_following_hashtags_unsupported">此实例不支持下列话题标签。</string> + <string name="title_followed_hashtags">关注的话题标签</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + <string name="pref_default_post_language">发嘟文默认使用的语言</string> + <string name="description_post_edited">已编辑</string> + <string name="post_edited">编辑了 %1$s</string> + <string name="error_muting_hashtag_format">对 #%1$s 静音时出错</string> + <string name="error_unmuting_hashtag_format">对 #%1$s 取消静音时出错</string> + <string name="hint_media_description_missing">媒体文件应该有描述。</string> + <string name="pref_title_http_proxy_port_message">端口应在 %1$d 和 %2$d 之间</string> + <string name="error_status_source_load">从服务器加载状态源失败。</string> + <string name="post_media_alt">ALT</string> + <string name="action_discard">放弃更改</string> + <string name="action_continue_edit">继续编辑</string> + <string name="compose_unsaved_changes">你有未保存的更改。</string> + <string name="status_created_info">%1$s 创建了</string> + <string name="mute_notifications_switch">将通知静音</string> + <string name="title_edits">编辑</string> + <string name="status_edit_info">%1$s 编辑了</string> + <string name="a11y_label_loading_thread">加载帖子</string> + <string name="action_share_account_link">分享账户链接</string> + <string name="action_share_account_username">分享账户用户名</string> + <string name="send_account_link_to">分享账户链接到…</string> + <string name="send_account_username_to">分享账户用户名到…</string> + <string name="account_username_copied">已复制用户名</string> + <string name="pref_title_reading_order">阅读顺序</string> + <string name="pref_summary_http_proxy_disabled">已禁用</string> + <string name="pref_summary_http_proxy_missing"><未设置></string> + <string name="pref_summary_http_proxy_invalid"><无效></string> + <string name="pref_reading_order_newest_first">从新到旧</string> + <string name="pref_reading_order_oldest_first">从旧到新</string> + <string name="action_browser_login">用浏览器登录</string> + <string name="description_login">多数情况下有效。没有数据泄露给其他应用。</string> + <string name="description_browser_login">可能支持额外的验证方法但需要受支持的浏览器。</string> + <string name="action_post_failed_detail_plural">你的嘟文上传失败,已被保存到草稿。 +\n +\n要么是无法联系服务器,要么是服务器拒绝了它。</string> + <string name="action_post_failed_show_drafts">显示草稿</string> + <string name="action_post_failed_do_nothing">取消</string> + <string name="action_post_failed">上传失败了</string> + <string name="action_post_failed_detail">你的嘟文上传失败,已被保存到草稿。 +\n +\n要么是无法联系服务器,要么是服务器拒绝了它。</string> + <string name="accessibility_talking_about_tag">%1$d 人正谈论话题标签 %2$s</string> + <string name="title_public_trending_hashtags">热门话题标签</string> + <string name="total_usage">总使用</string> + <string name="total_accounts">总账户</string> + <string name="dialog_follow_hashtag_hint">#hashtag</string> + <string name="dialog_follow_hashtag_title">关注话题标签</string> + <string name="action_refresh">刷新</string> + <string name="notification_unknown_name">未知</string> + <string name="socket_timeout_exception">联系你的服务器花了太长时间</string> + <string name="ui_error_unknown">未知原因</string> + <string name="ui_error_clear_notifications">未能清除通知:%1$s</string> + <string name="ui_error_bookmark">未能将嘟文将入书签:%1$s</string> + <string name="ui_error_favourite">收藏嘟文失败:%1$s</string> + <string name="ui_error_reblog">转发嘟文失败:%1$s</string> + <string name="ui_error_vote">投票失败:%1$s</string> + <string name="ui_error_accept_follow_request">未能接受关注请求:%1$s</string> + <string name="ui_error_reject_follow_request">未能拒绝关注请求:%1$s</string> + <string name="ui_success_accepted_follow_request">关注请求被接受</string> + <string name="ui_success_rejected_follow_request">关注请求被拦截</string> + <string name="status_filtered_show_anyway">仍要显示</string> + <string name="status_filter_placeholder_label_format">已过滤:%1$s</string> + <string name="pref_title_account_filter_keywords">个人资料</string> + <string name="hint_filter_title">我的筛选器</string> + <string name="label_filter_title">标题</string> + <string name="filter_action_warn">警告</string> + <string name="filter_action_hide">隐藏</string> + <string name="filter_description_warn">隐藏但显示警告信息</string> + <string name="label_filter_action">过滤操作</string> + <string name="label_filter_context">筛选器上下文</string> + <string name="action_add">添加</string> + <string name="filter_keyword_display_format">%1$s (整词)</string> + <string name="filter_keyword_addition_title">添加关键词</string> + <string name="filter_edit_keyword_title">编辑关键词</string> + <string name="filter_description_format">%1$s: %2$s</string> + <string name="filter_description_hide">完全隐藏</string> + <string name="label_filter_keywords">关键词或短语过滤</string> + <string name="pref_title_show_stat_inline">在时间线中显示嘟文统计数字</string> + <string name="help_empty_home">这是你的 <b>主时间线</b>。它展示你所关注账户最近发表的嘟文。 +\n +\n要探索账户, 你可以浏览其他时间线。比如,你的账户所在实例服务器的本地时间线 [iconics gmd_group]。你也可以按名称 [iconics gmd_search]进行搜索;比如,搜索Tusky 来寻找我们的 Mastodon 账户。</string> + <string name="post_media_image">图片</string> + <string name="select_list_manage">管理列表</string> + <string name="load_newest_notifications">加载最新通知</string> + <string name="compose_delete_draft">删除草稿?</string> + <string name="error_missing_edits">你的服务器知晓这篇帖子被编辑,但没有编辑的副本,所以无法呈现给你。 +\n +\n这是一个 <a href="https://github.com/mastodon/mastodon/issues/25398">Mastodon issue #25398</a>。</string> + <string name="pref_ui_text_size">用户界面文本大小</string> + <string name="notification_listenable_worker_description">Tusky 在后台工作时进行提示</string> + <string name="notification_notification_worker">获取通知中…</string> + <string name="notification_prune_cache">缓存维护…</string> + <string name="notification_listenable_worker_name">后台活动</string> + <string name="error_media_upload_sending_fmt">上传失败了:%1$s</string> + <string name="about_account_info_title">你的账户</string> + <string name="about_account_info">\@%1$s@%2$s +\n版本: %3$s</string> + <string name="about_copied">已复制版本和设备信息</string> + <string name="about_device_info_title">你的设备</string> + <string name="about_device_info">%1$s %2$s +\nAndroid 系统版本: %3$s +\nSDK 版本: %4$d</string> + <string name="about_copy">复制版本及设备信息</string> + <string name="list_exclusive_label">不出现在首页时间线中</string> + <string name="error_media_playback">播放失败了:%1$s</string> + <string name="dialog_delete_filter_text">删除筛选器\'%1$s\'吗?</string> + <string name="dialog_delete_filter_positive_action">删除</string> + <string name="dialog_save_profile_changes_message">你要保存你的个人资料更改吗?</string> + <string name="help_empty_conversations">这是你的 <b>私信</b>;有时也称为对话或直接消息 (DM)。 +\n +\n私信需要将嘟文的可见性 [iconics gmd_public] 设为 [iconics gmd_mail] <i>Direct</i> 并在文本中提及一名或多名用户。 +\n +\n你可以在账户的个人资料视图中,轻按“创建”按钮 [iconics gmd_edit] 并更改可见性。 </string> + <string name="muting_hashtag_success_format">以警告形式对话题标签 #%1$s 静音</string> + <string name="unmuting_hashtag_success_format">取消对话题标签 #%1$s 的静音</string> + <string name="action_view_filter">查看筛选器</string> + <string name="following_hashtag_success_format">正在关注话题标签 #%1$s</string> + <string name="unfollowing_hashtag_success_format">已不再关注话题标签 #%1$s</string> + <string name="help_empty_lists">这是你的 <b>列表视图</b>。你可以定义私人列表并添加联系人到列表中。 +\n +\n 请注意,你只能将你已关注的账户添加到列表中。 +\n +\n 这些列表可被用作“账户选项” [iconics gmd_account_circle] [iconics gmd_navigate_next] 选项卡中的标签页。 </string> + <string name="error_blocking_domain">未能静音 %1$s: %2$s</string> + <string name="error_unblocking_domain">未能取消对 %1$s: %2$s的静音</string> + <string name="label_image">图片</string> + <string name="app_theme_system_black">使用系统设计(黑色)</string> + <string name="title_public_trending_statuses">走红嘟文</string> + <string name="list_reply_policy_none">一个也没有</string> + <string name="list_reply_policy_list">列表成员</string> + <string name="list_reply_policy_followed">任何已关注的用户</string> + <string name="list_reply_policy_label">显示哪些回复</string> + <string name="pref_title_show_self_boosts_description">某人转发自己的嘟文</string> + <string name="pref_title_show_self_boosts">显示自转发嘟文</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml new file mode 100644 index 0000000..ad8a0fd --- /dev/null +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -0,0 +1,497 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">應用程式出現異常。</string> + <string name="error_network">網絡請求出錯,請檢查互聯網連接並重試!</string> + <string name="error_empty">內容不能為空。</string> + <string name="error_invalid_domain">該域名無效</string> + <string name="error_failed_app_registration">無法連接此伺服器。</string> + <string name="error_no_web_browser_found">沒有可用的瀏覽器。</string> + <string name="error_authorization_unknown">認證過程出現未知錯誤。</string> + <string name="error_authorization_denied">授權被拒絕。</string> + <string name="error_retrieving_oauth_token">無法獲取登入資訊。</string> + <string name="error_compose_character_limit">嘟文太長了!</string> + <string name="error_media_upload_type">無法上傳此類型的檔案。</string> + <string name="error_media_upload_opening">此檔案無法開啟。</string> + <string name="error_media_upload_permission">需要授予 Tusky 讀取媒體檔案的權限。</string> + <string name="error_media_download_permission">需要授予 Tusky 寫入儲存空間的權限。</string> + <string name="error_media_upload_image_or_video">無法在嘟文中同時插入影片和圖片。</string> + <string name="error_media_upload_sending">媒體檔案上傳失敗。</string> + <string name="error_sender_account_gone">嘟文發送時出錯。</string> + <string name="title_home">主頁</string> + <string name="title_notifications">通知設定</string> + <string name="title_public_local">本站時間軸</string> + <string name="title_public_federated">跨站公開時間軸</string> + <string name="title_direct_messages">私信</string> + <string name="title_tab_preferences">標籤頁</string> + <string name="title_view_thread">嘟文</string> + <string name="title_posts">嘟文</string> + <string name="title_posts_with_replies">嘟文和回覆</string> + <string name="title_posts_pinned">已置頂</string> + <string name="title_follows">正在關注</string> + <string name="title_followers">關注者</string> + <string name="title_favourites">我的收藏</string> + <string name="title_mutes">被靜音的使用者</string> + <string name="title_blocks">被封鎖的使用者</string> + <string name="title_follow_requests">關注請求</string> + <string name="title_edit_profile">編輯個人資料</string> + <string name="title_drafts">草稿</string> + <string name="title_licenses">開源授權</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s 轉嘟了</string> + <string name="post_sensitive_media_title">敏感內容</string> + <string name="post_media_hidden_title">已隱藏的照片或影片</string> + <string name="post_sensitive_media_directions">點一下顯示</string> + <string name="post_content_warning_show_more">顯示更多</string> + <string name="post_content_warning_show_less">摺疊內容</string> + <string name="post_content_show_more">展開</string> + <string name="post_content_show_less">摺疊</string> + <string name="message_empty">沒有內容。</string> + <string name="footer_empty">還沒有內容,向下拉動即可重新整理!</string> + <string name="notification_reblog_format">%1$s 轉嘟了你的嘟文</string> + <string name="notification_favourite_format">%1$s 把你的嘟文加入了最愛</string> + <string name="notification_follow_format">%1$s 關注了你</string> + <string name="report_username_format">檢舉使用者 @%1$s 的濫用行為</string> + <string name="report_comment_hint">更多評論?</string> + <string name="action_quick_reply">快速回覆</string> + <string name="action_reply">回覆</string> + <string name="action_reblog">轉嘟</string> + <string name="action_unreblog">取消轉嘟</string> + <string name="action_favourite">收藏</string> + <string name="action_unfavourite">取消收藏</string> + <string name="action_more">更多</string> + <string name="action_compose">撰寫嘟文</string> + <string name="action_login">登入 Mastodon 帳號</string> + <string name="action_logout">登出</string> + <string name="action_logout_confirm">確定要登出 %1$s 嗎?</string> + <string name="action_follow">關注</string> + <string name="action_unfollow">取消關注</string> + <string name="action_block">封鎖</string> + <string name="action_unblock">取消封鎖</string> + <string name="action_hide_reblogs">隱藏轉嘟</string> + <string name="action_show_reblogs">顯示轉嘟</string> + <string name="action_report">檢舉</string> + <string name="action_delete">刪除</string> + <string name="action_delete_and_redraft">刪除並重新編輯</string> + <string name="action_send">嘟嘟</string> + <string name="action_send_public">嘟嘟!</string> + <string name="action_retry">重試</string> + <string name="action_close">關閉</string> + <string name="action_view_profile">個人資料</string> + <string name="action_view_preferences">設定</string> + <string name="action_view_account_preferences">帳戶設定</string> + <string name="action_view_favourites">我的收藏</string> + <string name="action_view_mutes">被靜音的使用者</string> + <string name="action_view_blocks">被封鎖的使用者</string> + <string name="action_view_follow_requests">關注請求</string> + <string name="action_view_media">媒體</string> + <string name="action_open_in_web">在瀏覽器中開啟</string> + <string name="action_add_media">從相簿中選擇</string> + <string name="action_photo_take">拍照</string> + <string name="action_share">分享</string> + <string name="action_mute">靜音</string> + <string name="action_unmute">取消靜音</string> + <string name="action_mention">提及</string> + <string name="action_hide_media">隱藏媒體檔案</string> + <string name="action_open_drawer">打開應用抽屜</string> + <string name="action_save">儲存</string> + <string name="action_edit_profile">編輯個人資料</string> + <string name="action_edit_own_profile">編輯</string> + <string name="action_undo">復原</string> + <string name="action_accept">接受</string> + <string name="action_reject">拒絕</string> + <string name="action_search">搜尋</string> + <string name="action_access_drafts">草稿</string> + <string name="action_toggle_visibility">設定嘟文可見範圍</string> + <string name="action_content_warning">設定敏感內容警告</string> + <string name="action_emoji_keyboard">插入表情符號</string> + <string name="action_add_tab">新增標籤頁</string> + <string name="action_links">連結</string> + <string name="action_mentions">提及</string> + <string name="action_hashtags">話題</string> + <string name="action_open_reblogger">打開轉嘟用戶主頁</string> + <string name="action_open_reblogged_by">顯示轉嘟</string> + <string name="action_open_faved_by">顯示最愛</string> + <string name="title_hashtags_dialog">話題</string> + <string name="title_mentions_dialog">提及</string> + <string name="title_links_dialog">連結</string> + <string name="action_open_media_n">打開媒體 #%1$d</string> + <string name="download_image">正在下載 %1$s</string> + <string name="action_copy_link">複製連結</string> + <string name="action_open_as">打開為 %1$s</string> + <string name="action_share_as">分享為 …</string> + <string name="download_media">下載媒體</string> + <string name="downloading_media">正在下載媒體</string> + <string name="send_post_link_to">分享連結到…</string> + <string name="send_post_content_to">分享嘟文到…</string> + <string name="send_media_to">分享媒體到…</string> + <string name="confirmation_reported">已檢舉!</string> + <string name="confirmation_unblocked">已解除封鎖</string> + <string name="confirmation_unmuted">已解除靜音</string> + <string name="hint_domain">哪一個域名?</string> + <string name="hint_compose">有什麼新鮮事?</string> + <string name="hint_content_warning">敏感內容警告</string> + <string name="hint_display_name">暱稱</string> + <string name="hint_note">簡介</string> + <string name="hint_search">搜尋…</string> + <string name="search_no_results">沒找到結果</string> + <string name="label_quick_reply">回覆…</string> + <string name="label_avatar">頭像</string> + <string name="label_header">標題</string> + <string name="link_whats_an_instance">什麼是站點?</string> + <string name="login_connection">正在連線…</string> + <string name="dialog_whats_an_instance">輸入你帳號所在的 Mastodon 站點的域名或地址,譬如 mastodon.social、icosahedron.website、social.tchncs.de 和 <a href="https://instances.social">更多</a> +\n +\n如果你還沒有帳號,你可以輸入你想要加入的域名並在此建立新帳號。 +\n +\n一個站點是一個託管你的帳號的地方,但是你可以很容易的跟不同站台的人們交流,就像是在同一個站台一樣。 +\n +\n更多資訊可以在 <a href="https://joinmastodon.org">joinmastodon.org</a> 查看。 </string> + <string name="dialog_title_finishing_media_upload">正在完成上傳</string> + <string name="dialog_message_uploading_media">正在上傳…</string> + <string name="dialog_download_image">下載</string> + <string name="dialog_message_cancel_follow_request">移除關注請求?</string> + <string name="dialog_unfollow_warning">停止關注此使用者?</string> + <string name="dialog_delete_post_warning">刪除這條嘟文?</string> + <string name="dialog_redraft_post_warning">刪除並重新編輯這條嘟文?</string> + <string name="visibility_public">公開:所有人可見,並會出現在公開時間軸上</string> + <string name="visibility_unlisted">不公開:所有人可見,但不會出現在公開時間軸上</string> + <string name="visibility_private">僅關注者:只有經過你確認後關注你的使用者可見</string> + <string name="visibility_direct">私信:只有被提及的使用者可見</string> + <string name="pref_title_edit_notification_settings">通知</string> + <string name="pref_title_notifications_enabled">通知</string> + <string name="pref_title_notification_alerts">提醒</string> + <string name="pref_title_notification_alert_sound">通知鈴聲</string> + <string name="pref_title_notification_alert_vibrate">振動</string> + <string name="pref_title_notification_alert_light">呼吸燈</string> + <string name="pref_title_notification_filters">事件</string> + <string name="pref_title_notification_filter_mentions">被提及</string> + <string name="pref_title_notification_filter_follows">有新的關注者</string> + <string name="pref_title_notification_filter_reblogs">嘟文被轉嘟</string> + <string name="pref_title_notification_filter_favourites">嘟文被加入收藏</string> + <string name="pref_title_notification_filter_poll">投票已結束</string> + <string name="pref_title_appearance_settings">外觀</string> + <string name="pref_title_app_theme">佈景主題</string> + <string name="pref_title_timelines">時間軸</string> + <string name="pref_title_timeline_filters">過濾器</string> + <string name="app_them_dark">黑夜</string> + <string name="app_theme_light">白天</string> + <string name="app_theme_black">暗色</string> + <string name="app_theme_auto">自動切換</string> + <string name="app_theme_system">跟隨系統</string> + <string name="pref_title_browser_settings">瀏覽器</string> + <string name="pref_title_custom_tabs">使用 Chrome Custom Tabs</string> + <string name="pref_title_language">語言</string> + <string name="pref_title_post_filter">時間軸過濾</string> + <string name="pref_title_post_tabs">標籤頁</string> + <string name="pref_title_show_boosts">顯示轉嘟</string> + <string name="pref_title_show_replies">顯示回覆</string> + <string name="pref_title_show_media_preview">顯示預覽圖</string> + <string name="pref_title_proxy_settings">代理伺服器</string> + <string name="pref_title_http_proxy_settings">HTTP 代理伺服器</string> + <string name="pref_title_http_proxy_enable">啟用 HTTP 代理伺服器</string> + <string name="pref_title_http_proxy_server">HTTP 代理伺服器</string> + <string name="pref_title_http_proxy_port">HTTP 代理伺服器埠</string> + <string name="pref_default_post_privacy">嘟文預設可見範圍</string> + <string name="pref_default_media_sensitivity">總是將媒體標示為敏感</string> + <string name="pref_publishing">發佈(與伺服器同步)</string> + <string name="pref_failed_to_sync">同步設定失敗</string> + <string name="post_privacy_public">公開</string> + <string name="post_privacy_unlisted">不公開</string> + <string name="post_privacy_followers_only">僅關注者</string> + <string name="pref_post_text_size">字體大小</string> + <string name="post_text_size_smallest">最小</string> + <string name="post_text_size_small">小</string> + <string name="post_text_size_medium">標準</string> + <string name="post_text_size_large">大</string> + <string name="post_text_size_largest">最大</string> + <string name="notification_mention_name">提及</string> + <string name="notification_mention_descriptions">當有使用者在嘟文中提及我時</string> + <string name="notification_follow_name">關注</string> + <string name="notification_follow_description">當有使用者關注我時</string> + <string name="notification_boost_name">轉嘟</string> + <string name="notification_boost_description">當有使用者轉嘟了我的嘟文時</string> + <string name="notification_favourite_name">收藏</string> + <string name="notification_favourite_description">當有使用者把我的嘟文加入收藏時</string> + <string name="notification_poll_name">投票</string> + <string name="notification_poll_description">當我參與的投票結束時</string> + <string name="notification_mention_format">%1$s 提及了你</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s 和 %4$d 人</string> + <string name="notification_summary_medium">%1$s, %2$s, 和 %3$s</string> + <string name="notification_summary_small">%1$s 和 %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="other">%1$d 個新互動</item> + </plurals> + <string name="description_account_locked">鎖嘟用戶</string> + <string name="about_title_activity">關於 Tusky</string> + <string name="about_tusky_license">Tusky 是基於 GNU General Public License Version 3 許可證開源的自由軟體完整的許可證協議:https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site"> + 專案網站:\n + https://tusky.app + </string> + <string name="about_bug_feature_request_site"> + 問題回報:\n + https://github.com/tuskyapp/Tusky/issues + </string> + <string name="about_tusky_account">Tusky 官方帳號</string> + <string name="post_share_content">分享嘟文內容</string> + <string name="post_share_link">分享嘟文連結</string> + <string name="post_media_images">照片</string> + <string name="post_media_video">影片</string> + <string name="state_follow_requested">已請求關注</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">%1$d 年內</string> + <string name="abbreviated_in_days">%1$d 天內</string> + <string name="abbreviated_in_hours">%1$d 小時內</string> + <string name="abbreviated_in_minutes">%1$d 分鐘內</string> + <string name="abbreviated_in_seconds">%1$d 秒內</string> + <string name="abbreviated_years_ago">%1$d 年前</string> + <string name="abbreviated_days_ago">%1$d 天前</string> + <string name="abbreviated_hours_ago">%1$d 小時前</string> + <string name="abbreviated_minutes_ago">%1$d 分鐘前</string> + <string name="abbreviated_seconds_ago">%1$d 秒前</string> + <string name="follows_you">關注了你</string> + <string name="pref_title_alway_show_sensitive_media">總是顯示所有敏感媒體內容</string> + <string name="title_media">媒體</string> + <string name="replying_to">回覆 @%1$s</string> + <string name="load_more_placeholder_text">載入更多</string> + <string name="pref_title_public_filter_keywords">公共時間軸</string> + <string name="pref_title_thread_filter_keywords">對話</string> + <string name="filter_addition_title">添加新的過濾器</string> + <string name="filter_edit_title">編輯過濾器</string> + <string name="filter_dialog_remove_button">移除</string> + <string name="filter_dialog_update_button">更新</string> + <string name="filter_add_description">需要過濾的文字</string> + <string name="add_account_name">加入帳號</string> + <string name="add_account_description">加入新的 Mastodon 帳號</string> + <string name="action_lists">列表</string> + <string name="title_lists">列表</string> + <string name="error_create_list">無法新建列表</string> + <string name="error_rename_list">無法重命名列表</string> + <string name="error_delete_list">無法刪除列表</string> + <string name="action_create_list">新建列表</string> + <string name="action_rename_list">重命名列表</string> + <string name="action_delete_list">刪除列表</string> + <string name="hint_search_people_list">搜索已關注的用戶</string> + <string name="action_add_to_list">添加用戶到列表</string> + <string name="action_remove_from_list">從列表中移除用戶</string> + <string name="compose_active_account_description">以 %1$s 發嘟文</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">為視覺障礙用戶提供的描述\n(限制 %1$d 字)</item> + </plurals> + <string name="action_set_caption">設定圖片標題</string> + <string name="action_remove">移除</string> + <string name="lock_account_label">保護你的帳戶(鎖嘟)</string> + <string name="lock_account_label_description">你需要手動審核所有關注請求</string> + <string name="compose_save_draft">儲存為草稿?</string> + <string name="send_post_notification_title">正在發送…</string> + <string name="send_post_notification_error_title">發送失敗</string> + <string name="send_post_notification_channel_name">嘟文發送中</string> + <string name="send_post_notification_cancel_title">發送已被取消</string> + <string name="send_post_notification_saved_content">嘟文已儲存為草稿</string> + <string name="action_compose_shortcut">新嘟文</string> + <string name="error_no_custom_emojis">當前站點 %1$s 沒有自訂表情符號</string> + <string name="emoji_style">表情符號風格</string> + <string name="system_default">系統預設</string> + <string name="download_fonts">你需要先下載這些表情符號包</string> + <string name="performing_lookup_title">正在查詢…</string> + <string name="expand_collapse_all_posts">展開/摺疊所有嘟文</string> + <string name="action_open_post">開啟嘟文</string> + <string name="restart_required">需要重啟應用程式</string> + <string name="restart_emoji">你需要重啟 Tusky 才能生效</string> + <string name="later">稍後</string> + <string name="restart">立即重啟</string> + <string name="caption_systememoji">系統預設的表情符號包</string> + <string name="caption_blobmoji">Android 4.4–7.1 的黃饅頭表情符號</string> + <string name="caption_twemoji">Mastodon 使用的表情符號</string> + <!-- string name="emoji_shortcode_format" translatable="false">:%1$s:</string --> + <string name="download_failed">下載失敗</string> + <string name="profile_badge_bot_text">機器人</string> + <string name="account_moved_description">%1$s 已遷移到:</string> + <string name="reblog_private">轉嘟(可見者不變)</string> + <string name="unreblog_private">取消轉嘟</string> + <string name="license_description">Tusky 使用了以下開源專案的原始碼:</string> + <string name="license_apache_2">以 Apache License 授權(詳見下方)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">個人資料附加資訊</string> + <string name="profile_metadata_add">新增資訊</string> + <string name="profile_metadata_label_label">標籤</string> + <string name="profile_metadata_content_label">內容</string> + <string name="pref_title_absolute_time">嘟文顯示精確時間</string> + <string name="label_remote_account">以下資訊可能並不完整,要檢視完整資料請使用瀏覽器開啟。</string> + <string name="unpin_action">取消置頂</string> + <string name="pin_action">置頂</string> + <plurals name="favs"> + <item quantity="other"><b>%1$s</b> 次收藏</item> + </plurals> + <plurals name="reblogs"> + <item quantity="other"><b>%1$s</b> 次轉嘟</item> + </plurals> + <string name="title_reblogged_by">轉嘟由</string> + <string name="title_favourited_by">收藏由</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s 和 %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s 和 %3$d 等人</string> + <string name="description_post_media"> + 媒體: %1$s + </string> + <string name="description_post_cw"> + 內容提醒: %1$s + </string> + <string name="description_post_media_no_description_placeholder"> + 沒有媒體描述信息 + </string> + <string name="description_post_reblogged"> + 被轉嘟 + </string> + <string name="description_post_favourited"> + 被收藏 + </string> + <string name="description_visibility_public"> + 公開 + </string> + <string name="description_visibility_unlisted"> + 不公開 + </string> + <string name="description_visibility_private"> + 僅關注者 + </string> + <string name="description_visibility_direct"> + 私信 + </string> + <string name="hint_list_name">列表名</string> + <string name="edit_hashtag_hint">話題名(不含前面的 # 號)</string> + <string name="notifications_clear">清空</string> + <string name="notifications_apply_filter">分類</string> + <string name="filter_apply">應用</string> + <string name="compose_shortcut_long_label">發表新嘟文</string> + <string name="compose_shortcut_short_label">新嘟文</string> + <string name="pref_title_bot_overlay">顯示機器人標誌</string> + <string name="notification_clear_text">你確定要永久清空通知列表嗎?</string> + <string name="poll_info_format"> + <!-- 15 votes • 1 hour left --> + %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="other">%1$s 次投票</item> + </plurals> + <string name="poll_info_time_absolute">%1$s 結束</string> + <string name="poll_info_closed">已結束</string> + <string name="poll_vote">投票</string> + <string name="poll_ended_voted">你參與的投票已結束</string> + <string name="poll_ended_created">你創建的投票已結束</string> + <!--These are for timestamps on polls --> + <plurals name="poll_timespan_days"> + <item quantity="other">剩餘 %1$d 天</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="other">剩餘 %1$d 小時</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="other">剩餘 %1$d 分鐘</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="other">剩餘 %1$d 秒</item> + </plurals> + <string name="edit_poll">編輯</string> + <string name="hashtags">話題</string> + <string name="notification_follow_request_name">關注請求</string> + <string name="action_edit">編輯</string> + <string name="pref_title_animate_custom_emojis">動態自訂表情符號</string> + <string name="pref_title_gradient_for_media">在隱藏的媒體上使用漸變色彩</string> + <string name="pref_title_animate_gif_avatars">動態 GIF 頭像</string> + <string name="pref_title_notification_filter_subscriptions">我關注的人有新嘟文</string> + <string name="pref_title_notification_filter_follow_requests">已送出關注請求</string> + <string name="dialog_mute_hide_notifications">隱藏通知</string> + <string name="dialog_mute_warning">靜音 @%1$s?</string> + <string name="dialog_block_warning">封鎖 @%1$s?</string> + <string name="mute_domain_warning_dialog_ok">隱藏整個網域</string> + <string name="mute_domain_warning">確定要封鎖 %1$s 所有內容?你將不會在任何公開時間軸或是通知中看到來自這個網域的內容。你的關注者若來自這個網域則將會被移除。</string> + <string name="confirmation_domain_unmuted">%1$s 已解除隱藏</string> + <string name="action_reset_schedule">重設</string> + <string name="action_schedule_post">排程嘟文</string> + <string name="action_access_scheduled_posts">排程的嘟文</string> + <string name="action_unmute_conversation">取消靜音對話</string> + <string name="action_mute_conversation">靜音對話</string> + <string name="action_unmute_domain">取消靜音 %1$s</string> + <string name="action_mute_domain">靜音 %1$s</string> + <string name="action_unmute_desc">取消靜音 %1$s</string> + <string name="action_add_poll">新增投票</string> + <string name="action_view_domain_mutes">被隱藏的網域</string> + <string name="description_post_bookmarked">被加入書籤</string> + <string name="action_view_bookmarks">我的書籤</string> + <string name="action_bookmark">書籤</string> + <string name="title_bookmarks">我的書籤</string> + <string name="notification_subscription_format">%1$s 剛剛發了新嘟文</string> + <string name="notification_follow_request_format">%1$s 希望可以關注你</string> + <string name="title_announcements">公告</string> + <string name="title_scheduled_posts">已排程的嘟文</string> + <string name="title_domain_mutes">被隱藏的網域</string> + <string name="filter_dialog_whole_word">完整字詞</string> + <string name="drafts_post_reply_removed">你的草稿欲回覆的原嘟文已被刪除</string> + <string name="draft_deleted">草稿已刪除</string> + <string name="drafts_failed_loading_reply">載入回覆資訊失敗</string> + <string name="drafts_post_failed_to_send">這條嘟文發送失敗!</string> + <string name="dialog_delete_list_warning">你確定要刪除列表 %1$s?</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="other">你無法上傳超過 %1$d 媒體附件。</item> + </plurals> + <string name="account_note_saved">已儲存!</string> + <string name="account_note_hint">你對此帳號的個人註記</string> + <string name="pref_title_hide_top_toolbar">隱藏頂端工具列的標題</string> + <string name="pref_title_confirm_reblogs">在轉嘟時提示確認</string> + <string name="pref_title_show_cards_in_timelines">在時間軸中顯示連結預覽</string> + <string name="warning_scheduling_interval">Mastodon 的最短發文間隔限制為 5 分鐘。</string> + <string name="no_announcements">沒有公告。</string> + <string name="no_scheduled_posts">你沒有任何已排程的嘟文。</string> + <string name="no_drafts">你沒有任何草稿。</string> + <string name="post_lookup_error_format">尋找嘟文時發生錯誤 %1$s</string> + <string name="poll_new_choice_hint">選項 %1$d</string> + <string name="poll_allow_multiple_choices">多個選項</string> + <string name="add_poll_choice">新增選項</string> + <string name="duration_7_days">7 天</string> + <string name="duration_3_days">3 天</string> + <string name="duration_1_day">1 天</string> + <string name="duration_6_hours">6 小時</string> + <string name="duration_1_hour">1 小時</string> + <string name="duration_30_min">30 分鐘</string> + <string name="duration_5_min">5 分鐘</string> + <string name="duration_indefinite">無限期</string> + <string name="label_duration">期間</string> + <string name="create_poll_title">投票</string> + <string name="pref_title_enable_swipe_for_tabs">啟用在分頁間切換的滑動手勢</string> + <string name="failed_search">搜尋失敗</string> + <string name="title_accounts">帳號</string> + <string name="failed_fetch_posts">擷取狀態失敗</string> + <string name="failed_report">回報失敗</string> + <string name="report_remote_instance">轉送至 %1$s</string> + <string name="hint_additional_info">額外的評論</string> + <string name="report_sent_success">成功回報 @%1$s</string> + <string name="button_done">完成</string> + <string name="button_back">返回</string> + <string name="button_continue">繼續</string> + <plurals name="poll_info_people"> + <item quantity="other">%1$s 人</item> + </plurals> + <string name="list">列表</string> + <string name="select_list_title">選擇列表</string> + <string name="add_hashtag_title">加上話題標籤</string> + <string name="description_poll">投票選項: %1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="caption_notoemoji">Google 目前的表情符號包</string> + <string name="pref_title_alway_open_spoiler">總是顯示被標注為內容警告的嘟文</string> + <string name="post_media_attachments">附件</string> + <string name="post_media_audio">錄音</string> + <string name="about_powered_by_tusky">由 Tusky 提供</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="notification_subscription_description">當你關注的人發布新嘟文時通知</string> + <string name="notification_subscription_name">新嘟文</string> + <string name="notification_follow_request_description">關注請求的通知</string> + <string name="pref_main_nav_position_option_bottom">底端</string> + <string name="pref_main_nav_position_option_top">頂端</string> + <string name="pref_main_nav_position">主要導覽列的位置</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-zh-rMO/strings.xml b/app/src/main/res/values-zh-rMO/strings.xml new file mode 100644 index 0000000..a0a64dc --- /dev/null +++ b/app/src/main/res/values-zh-rMO/strings.xml @@ -0,0 +1,397 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">應用程式出現異常</string> + <string name="error_network">網絡請求出錯,請檢查互聯網連接並重試</string> + <string name="error_empty">內容不能為空</string> + <string name="error_invalid_domain">該域名無效</string> + <string name="error_failed_app_registration">無法連接此伺服器</string> + <string name="error_no_web_browser_found">沒有可用的瀏覽器</string> + <string name="error_authorization_unknown">認證過程出現未知錯誤</string> + <string name="error_authorization_denied">授權被拒絕</string> + <string name="error_retrieving_oauth_token">無法獲取登入資訊</string> + <string name="error_compose_character_limit">嘟文太長了!</string> + <string name="error_media_upload_type">無法上傳此類型的檔案</string> + <string name="error_media_upload_opening">此檔案無法開啟</string> + <string name="error_media_upload_permission">需要授予 Tusky 讀取媒體檔案的權限</string> + <string name="error_media_download_permission">需要授予 Tusky 寫入儲存空間的權限</string> + <string name="error_media_upload_image_or_video">無法在嘟文中同時插入影片和圖片</string> + <string name="error_media_upload_sending">媒體檔案上傳失敗</string> + <string name="error_sender_account_gone">嘟文發送時出錯</string> + <string name="title_home">主頁</string> + <string name="title_notifications">通知</string> + <string name="title_public_local">本站時間軸</string> + <string name="title_public_federated">跨站公開時間軸</string> + <string name="title_direct_messages">私信</string> + <string name="title_tab_preferences">標籤頁</string> + <string name="title_view_thread">嘟文</string> + <string name="title_posts">嘟文</string> + <string name="title_posts_with_replies">嘟文和回覆</string> + <string name="title_posts_pinned">已置頂</string> + <string name="title_follows">正在關注</string> + <string name="title_followers">關注者</string> + <string name="title_favourites">我的收藏</string> + <string name="title_mutes">被靜音的使用者</string> + <string name="title_blocks">被封鎖的使用者</string> + <string name="title_follow_requests">關注請求</string> + <string name="title_edit_profile">編輯個人資料</string> + <string name="title_drafts">草稿</string> + <string name="title_licenses">開源授權</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s 轉嘟了</string> + <string name="post_sensitive_media_title">敏感內容</string> + <string name="post_media_hidden_title">已隱藏的照片或影片</string> + <string name="post_sensitive_media_directions">點一下顯示</string> + <string name="post_content_warning_show_more">顯示更多</string> + <string name="post_content_warning_show_less">摺疊內容</string> + <string name="post_content_show_more">展開</string> + <string name="post_content_show_less">摺疊</string> + <string name="message_empty">沒有內容</string> + <string name="footer_empty">還沒有內容,向下拉動即可重新整理</string> + <string name="notification_reblog_format">%1$s 轉嘟了你的嘟文</string> + <string name="notification_favourite_format">%1$s 收藏了你的嘟文</string> + <string name="notification_follow_format">%1$s 關注了你</string> + <string name="report_username_format">檢舉使用者 @%1$s 的濫用行為</string> + <string name="report_comment_hint">更多評論?</string> + <string name="action_quick_reply">快速回覆</string> + <string name="action_reply">回覆</string> + <string name="action_reblog">轉嘟</string> + <string name="action_unreblog">取消轉嘟</string> + <string name="action_favourite">收藏</string> + <string name="action_unfavourite">取消收藏</string> + <string name="action_more">更多</string> + <string name="action_compose">新嘟文</string> + <string name="action_login">登入 Mastodon 帳號</string> + <string name="action_logout">登出</string> + <string name="action_logout_confirm">確定要登出 %1$s 嗎?</string> + <string name="action_follow">關注</string> + <string name="action_unfollow">取消關注</string> + <string name="action_block">封鎖</string> + <string name="action_unblock">取消封鎖</string> + <string name="action_hide_reblogs">隱藏轉嘟</string> + <string name="action_show_reblogs">顯示轉嘟</string> + <string name="action_report">檢舉</string> + <string name="action_delete">刪除</string> + <string name="action_delete_and_redraft">刪除並重新編輯</string> + <string name="action_send">嘟嘟</string> + <string name="action_send_public">嘟嘟!</string> + <string name="action_retry">重試</string> + <string name="action_close">關閉</string> + <string name="action_view_profile">個人資料</string> + <string name="action_view_preferences">設定</string> + <string name="action_view_account_preferences">帳戶設定</string> + <string name="action_view_favourites">我的收藏</string> + <string name="action_view_mutes">被靜音的使用者</string> + <string name="action_view_blocks">被封鎖的使用者</string> + <string name="action_view_follow_requests">關注請求</string> + <string name="action_view_media">媒體</string> + <string name="action_open_in_web">在瀏覽器中開啟</string> + <string name="action_add_media">從相簿中選擇</string> + <string name="action_photo_take">拍照</string> + <string name="action_share">分享</string> + <string name="action_mute">靜音</string> + <string name="action_unmute">取消靜音</string> + <string name="action_mention">提及</string> + <string name="action_hide_media">隱藏媒體檔案</string> + <string name="action_open_drawer">打開應用抽屜</string> + <string name="action_save">儲存</string> + <string name="action_edit_profile">編輯個人資料</string> + <string name="action_edit_own_profile">編輯</string> + <string name="action_undo">復原</string> + <string name="action_accept">接受</string> + <string name="action_reject">拒絕</string> + <string name="action_search">搜尋</string> + <string name="action_access_drafts">草稿</string> + <string name="action_toggle_visibility">設定嘟文可見範圍</string> + <string name="action_content_warning">敏感內容警告</string> + <string name="action_emoji_keyboard">插入表情符號</string> + <string name="action_add_tab">新增標籤頁</string> + <string name="action_links">連結</string> + <string name="action_mentions">提及</string> + <string name="action_hashtags">話題</string> + <string name="action_open_reblogger">打開轉嘟用戶主頁</string> + <string name="action_open_reblogged_by">顯示轉嘟</string> + <string name="action_open_faved_by">顯示收藏</string> + <string name="title_hashtags_dialog">話題</string> + <string name="title_mentions_dialog">提及</string> + <string name="title_links_dialog">連結</string> + <string name="action_open_media_n">打開媒體 #%1$d</string> + <string name="download_image">正在下載 %1$s…</string> + <string name="action_copy_link">複製連結</string> + <string name="action_open_as">打開為 %1$s</string> + <string name="action_share_as">分享為 …</string> + <string name="download_media">下載媒體</string> + <string name="downloading_media">正在下載媒體</string> + <string name="send_post_link_to">分享連結到…</string> + <string name="send_post_content_to">分享嘟文到…</string> + <string name="send_media_to">分享媒體到…</string> + <string name="confirmation_reported">已發送!</string> + <string name="confirmation_unblocked">已解除封鎖</string> + <string name="confirmation_unmuted">已解除靜音</string> + <string name="hint_domain">域名</string> + <string name="hint_compose">有什麼新鮮事?</string> + <string name="hint_content_warning">敏感內容警告</string> + <string name="hint_display_name">暱稱</string> + <string name="hint_note">簡介</string> + <string name="hint_search">搜尋…</string> + <string name="search_no_results">沒找到結果</string> + <string name="label_quick_reply">回覆…</string> + <string name="label_avatar">頭像</string> + <string name="label_header">標題</string> + <string name="link_whats_an_instance">什麼是站點?</string> + <string name="login_connection">正在連線…</string> + <string name="dialog_whats_an_instance">請輸入你帳號所在的 Mastodon 站點的域名或地址</string> + <string name="dialog_title_finishing_media_upload">正在完成上傳…</string> + <string name="dialog_message_uploading_media">正在上傳…</string> + <string name="dialog_download_image">下載</string> + <string name="dialog_message_cancel_follow_request">移除關注請求?</string> + <string name="dialog_unfollow_warning">停止關注此使用者?</string> + <string name="dialog_delete_post_warning">刪除這條嘟文?</string> + <string name="dialog_redraft_post_warning">刪除並重新編輯這條嘟文?</string> + <string name="visibility_public">公開:所有人可見,並會出現在公開時間軸上</string> + <string name="visibility_unlisted">不公開:所有人可見,但不會出現在公開時間軸上</string> + <string name="visibility_private">僅關注者:只有經過你確認後關注你的使用者可見</string> + <string name="visibility_direct">私信:只有被提及的使用者可見</string> + <string name="pref_title_edit_notification_settings">通知</string> + <string name="pref_title_notifications_enabled">通知</string> + <string name="pref_title_notification_alerts">提醒</string> + <string name="pref_title_notification_alert_sound">通知鈴聲</string> + <string name="pref_title_notification_alert_vibrate">振動</string> + <string name="pref_title_notification_alert_light">呼吸燈</string> + <string name="pref_title_notification_filters">事件</string> + <string name="pref_title_notification_filter_mentions">被提及</string> + <string name="pref_title_notification_filter_follows">有新的關注者</string> + <string name="pref_title_notification_filter_reblogs">嘟文被轉嘟</string> + <string name="pref_title_notification_filter_favourites">嘟文被收藏</string> + <string name="pref_title_notification_filter_poll">投票已結束</string> + <string name="pref_title_appearance_settings">外觀</string> + <string name="pref_title_app_theme">佈景主題</string> + <string name="pref_title_timelines">時間軸</string> + <string name="pref_title_timeline_filters">過濾器</string> + <string name="app_them_dark">黑夜</string> + <string name="app_theme_light">白天</string> + <string name="app_theme_black">暗色</string> + <string name="app_theme_auto">自動切換</string> + <string name="app_theme_system">跟隨系統</string> + <string name="pref_title_browser_settings">瀏覽器</string> + <string name="pref_title_custom_tabs">使用 Chrome Custom Tabs</string> + <string name="pref_title_language">語言</string> + <string name="pref_title_post_filter">時間軸過濾</string> + <string name="pref_title_post_tabs">標籤頁</string> + <string name="pref_title_show_boosts">顯示轉嘟</string> + <string name="pref_title_show_replies">顯示回覆</string> + <string name="pref_title_show_media_preview">顯示預覽圖</string> + <string name="pref_title_proxy_settings">代理伺服器</string> + <string name="pref_title_http_proxy_settings">HTTP 代理伺服器</string> + <string name="pref_title_http_proxy_enable">啟用 HTTP 代理伺服器</string> + <string name="pref_title_http_proxy_server">HTTP 代理伺服器</string> + <string name="pref_title_http_proxy_port">HTTP 代理伺服器埠</string> + <string name="pref_default_post_privacy">嘟文預設可見範圍</string> + <string name="pref_default_media_sensitivity">總是將媒體標示為敏感</string> + <string name="pref_publishing">發佈(與伺服器同步)</string> + <string name="pref_failed_to_sync">同步設定失敗</string> + <string name="post_privacy_public">公開</string> + <string name="post_privacy_unlisted">不公開</string> + <string name="post_privacy_followers_only">僅關注者</string> + <string name="pref_post_text_size">字體大小</string> + <string name="post_text_size_smallest">最小</string> + <string name="post_text_size_small">小</string> + <string name="post_text_size_medium">標準</string> + <string name="post_text_size_large">大</string> + <string name="post_text_size_largest">最大</string> + <string name="notification_mention_name">提及</string> + <string name="notification_mention_descriptions">當有使用者在嘟文中提及我時</string> + <string name="notification_follow_name">關注</string> + <string name="notification_follow_description">當有使用者關注我時</string> + <string name="notification_boost_name">轉嘟</string> + <string name="notification_boost_description">當有使用者轉嘟了我的嘟文時</string> + <string name="notification_favourite_name">收藏</string> + <string name="notification_favourite_description">當有使用者收藏了我的嘟文時</string> + <string name="notification_poll_name">投票</string> + <string name="notification_poll_description">當我參與的投票結束時</string> + <string name="notification_mention_format">%1$s 提及了你</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s 和 %4$d 人</string> + <string name="notification_summary_medium">%1$s, %2$s, 和 %3$s</string> + <string name="notification_summary_small">%1$s 和 %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="other">%1$d 個新互動</item> + </plurals> + <string name="description_account_locked">鎖嘟用戶</string> + <string name="about_title_activity">關於 Tusky</string> + <string name="about_tusky_license">Tusky 是基於 GNU General Public License Version 3 許可證開源的自由軟體完整的許可證協議:https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site"> + 專案網站:\n + https://tusky.app + </string> + <string name="about_bug_feature_request_site"> + 問題回報:\n + https://github.com/tuskyapp/Tusky/issues + </string> + <string name="about_tusky_account">Tusky 官方帳號</string> + <string name="post_share_content">分享嘟文內容</string> + <string name="post_share_link">分享嘟文連結</string> + <string name="post_media_images">照片</string> + <string name="post_media_video">影片</string> + <string name="state_follow_requested">已請求關注</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">%1$d 年內</string> + <string name="abbreviated_in_days">%1$d 天內</string> + <string name="abbreviated_in_hours">%1$d 小時內</string> + <string name="abbreviated_in_minutes">%1$d 分鐘內</string> + <string name="abbreviated_in_seconds">%1$d 秒內</string> + <string name="abbreviated_years_ago">%1$d 年前</string> + <string name="abbreviated_days_ago">%1$d 天前</string> + <string name="abbreviated_hours_ago">%1$d 小時前</string> + <string name="abbreviated_minutes_ago">%1$d 分鐘前</string> + <string name="abbreviated_seconds_ago">%1$d 秒前</string> + <string name="follows_you">關注了你</string> + <string name="pref_title_alway_show_sensitive_media">總是顯示所有敏感媒體內容</string> + <string name="title_media">媒體</string> + <string name="replying_to">回覆 @%1$s</string> + <string name="load_more_placeholder_text">載入更多</string> + <string name="pref_title_public_filter_keywords">公共時間軸</string> + <string name="pref_title_thread_filter_keywords">對話</string> + <string name="filter_addition_title">添加新的過濾器</string> + <string name="filter_edit_title">編輯過濾器</string> + <string name="filter_dialog_remove_button">移除</string> + <string name="filter_dialog_update_button">更新</string> + <string name="filter_add_description">需要過濾的文字</string> + <string name="add_account_name">加入帳號</string> + <string name="add_account_description">加入新的 Mastodon 帳號</string> + <string name="action_lists">列表</string> + <string name="title_lists">列表</string> + <string name="error_create_list">無法新建列表</string> + <string name="error_rename_list">無法重命名列表</string> + <string name="error_delete_list">無法刪除列表</string> + <string name="action_create_list">新建列表</string> + <string name="action_rename_list">重命名列表</string> + <string name="action_delete_list">刪除列表</string> + <string name="hint_search_people_list">搜索已關注的用戶</string> + <string name="action_add_to_list">添加用戶到列表</string> + <string name="action_remove_from_list">從列表中移除用戶</string> + <string name="compose_active_account_description">以 %1$s 發嘟文</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">為視覺障礙用戶提供的描述\n(限制 %1$d 字)</item> + </plurals> + <string name="action_set_caption">設定圖片標題</string> + <string name="action_remove">移除</string> + <string name="lock_account_label">保護你的帳戶(鎖嘟)</string> + <string name="lock_account_label_description">你需要手動審核所有關注請求</string> + <string name="compose_save_draft">儲存為草稿?</string> + <string name="send_post_notification_title">正在發送…</string> + <string name="send_post_notification_error_title">發送失敗</string> + <string name="send_post_notification_channel_name">嘟文發送中</string> + <string name="send_post_notification_cancel_title">發送已被取消</string> + <string name="send_post_notification_saved_content">嘟文已儲存為草稿</string> + <string name="action_compose_shortcut">發表新嘟文</string> + <string name="error_no_custom_emojis">當前站點 %1$s 沒有自訂表情符號</string> + <string name="emoji_style">表情符號風格</string> + <string name="system_default">系統預設</string> + <string name="download_fonts">你需要先下載這些表情符號包</string> + <string name="performing_lookup_title">正在查詢…</string> + <string name="expand_collapse_all_posts">展開/摺疊所有嘟文</string> + <string name="action_open_post">開啟嘟文</string> + <string name="restart_required">需要重啟應用程式</string> + <string name="restart_emoji">你需要重啟 Tusky 才能生效</string> + <string name="later">稍後</string> + <string name="restart">立即重啟</string> + <string name="caption_systememoji">系統預設的表情符號包</string> + <string name="caption_blobmoji">Android 4.4–7.1 的黃饅頭表情符號</string> + <string name="caption_twemoji">Mastodon 使用的表情符號</string> + <!-- string name="emoji_shortcode_format" translatable="false">:%1$s:</string --> + <string name="download_failed">下載失敗</string> + <string name="profile_badge_bot_text">機器人</string> + <string name="account_moved_description">%1$s 已遷移到:</string> + <string name="reblog_private">轉嘟(可見者不變)</string> + <string name="unreblog_private">取消轉嘟</string> + <string name="license_description">Tusky 使用了以下開源專案的原始碼:</string> + <string name="license_apache_2">以 Apache License 授權(詳見下方)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">個人資料附加資訊</string> + <string name="profile_metadata_add">新增資訊</string> + <string name="profile_metadata_label_label">標籤</string> + <string name="profile_metadata_content_label">內容</string> + <string name="pref_title_absolute_time">嘟文顯示精確時間</string> + <string name="label_remote_account">以下資訊可能並不完整,要檢視完整資料請使用瀏覽器開啟</string> + <string name="unpin_action">取消置頂</string> + <string name="pin_action">置頂</string> + <plurals name="favs"> + <item quantity="other"><b>%1$s</b> 次收藏</item> + </plurals> + <plurals name="reblogs"> + <item quantity="other"><b>%1$s</b> 次轉嘟</item> + </plurals> + <string name="title_reblogged_by">轉嘟</string> + <string name="title_favourited_by">收藏</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s 和 %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s 和 %3$d 等人</string> + <string name="description_post_media"> + 媒體: %1$s + </string> + <string name="description_post_cw"> + 內容提醒: %1$s + </string> + <string name="description_post_media_no_description_placeholder"> + 沒有媒體描述信息 + </string> + <string name="description_post_reblogged"> + 被轉嘟 + </string> + <string name="description_post_favourited"> + 被收藏 + </string> + <string name="description_visibility_public"> + 公開 + </string> + <string name="description_visibility_unlisted"> + 不公開 + </string> + <string name="description_visibility_private">關注者</string> + <string name="description_visibility_direct"> + 私信 + </string> + <string name="hint_list_name">列表名</string> + <string name="edit_hashtag_hint">話題名(不含前面的 # 號)</string> + <string name="notifications_clear">清空</string> + <string name="notifications_apply_filter">分類</string> + <string name="filter_apply">應用</string> + <string name="compose_shortcut_long_label">發表新嘟文</string> + <string name="compose_shortcut_short_label">發表新嘟文</string> + <string name="pref_title_bot_overlay">顯示機器人標誌</string> + <string name="notification_clear_text">你確定要永久清空通知列表嗎?</string> + <string name="poll_info_format"> + <!-- 15 votes • 1 hour left --> + %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="other">%1$s 次投票</item> + </plurals> + <string name="poll_info_time_absolute">%1$s 結束</string> + <string name="poll_info_closed">已結束</string> + <string name="poll_vote">投票</string> + <string name="poll_ended_voted">你參與的投票已結束</string> + <string name="poll_ended_created">你創建的投票已結束</string> + <!--These are for timestamps on polls --> + <plurals name="poll_timespan_days"> + <item quantity="other">剩餘 %1$d 天</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="other">剩餘 %1$d 小時</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="other">剩餘 %1$d 分鐘</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="other">剩餘 %1$d 秒</item> + </plurals> + <string name="notification_follow_request_name">關注請求</string> + <string name="hashtags">話題</string> + <string name="edit_poll">編輯</string> + <string name="action_edit">編輯</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-zh-rSG/strings.xml b/app/src/main/res/values-zh-rSG/strings.xml new file mode 100644 index 0000000..e6dc72c --- /dev/null +++ b/app/src/main/res/values-zh-rSG/strings.xml @@ -0,0 +1,400 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">应用程序出现异常</string> + <string name="error_network">网络请求出错,请检查互联网连接并重试</string> + <string name="error_empty">内容不能为空</string> + <string name="error_invalid_domain">该域名无效</string> + <string name="error_failed_app_registration">无法连接此服务器</string> + <string name="error_no_web_browser_found">没有可用的浏览器</string> + <string name="error_authorization_unknown">认证过程出现未知错误</string> + <string name="error_authorization_denied">授权被拒绝</string> + <string name="error_retrieving_oauth_token">无法获取登录信息</string> + <string name="error_compose_character_limit">嘟文太长了!</string> + <string name="error_media_upload_type">无法上传此类型的文件</string> + <string name="error_media_upload_opening">此文件无法打开</string> + <string name="error_media_upload_permission">需要授予 Tusky 读取媒体文件的权限</string> + <string name="error_media_download_permission">需要授予 Tusky 写入存储空间的权限</string> + <string name="error_media_upload_image_or_video">无法在嘟文中同时插入视频和图片</string> + <string name="error_media_upload_sending">媒体文件上传失败</string> + <string name="error_sender_account_gone">嘟文发送时出错</string> + <string name="title_home">主页</string> + <string name="title_notifications">通知</string> + <string name="title_public_local">本站时间轴</string> + <string name="title_public_federated">跨站公共时间轴</string> + <string name="title_direct_messages">私信</string> + <string name="title_tab_preferences">标签页</string> + <string name="title_view_thread">嘟文</string> + <string name="title_posts">嘟文</string> + <string name="title_posts_with_replies">嘟文和回复</string> + <string name="title_posts_pinned">已置顶</string> + <string name="title_follows">正在关注</string> + <string name="title_followers">关注者</string> + <string name="title_favourites">喜欢</string> + <string name="title_mutes">被隐藏的用户</string> + <string name="title_blocks">被屏蔽的用户</string> + <string name="title_follow_requests">关注请求</string> + <string name="title_edit_profile">编辑个人资料</string> + <string name="title_drafts">草稿</string> + <string name="title_licenses">开源协议</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s 转嘟了</string> + <string name="post_sensitive_media_title">敏感内容</string> + <string name="post_media_hidden_title">已隐藏的照片或视频</string> + <string name="post_sensitive_media_directions">点击显示</string> + <string name="post_content_warning_show_more">显示更多</string> + <string name="post_content_warning_show_less">折叠内容</string> + <string name="post_content_show_more">展开</string> + <string name="post_content_show_less">折叠</string> + <string name="message_empty">还没有内容</string> + <string name="footer_empty">还没有内容,向下拉动即可刷新</string> + <string name="notification_reblog_format">%1$s 转嘟了你的嘟文</string> + <string name="notification_favourite_format">%1$s 喜欢了你的嘟文</string> + <string name="notification_follow_format">%1$s 关注了你</string> + <string name="report_username_format">报告用户 @%1$s 的滥用行为</string> + <string name="report_comment_hint">报告更多信息</string> + <string name="action_quick_reply">快速回复</string> + <string name="action_reply">回复</string> + <string name="action_reblog">转嘟</string> + <string name="action_unreblog">取消转嘟</string> + <string name="action_favourite">喜欢</string> + <string name="action_unfavourite">取消喜欢</string> + <string name="action_more">更多</string> + <string name="action_compose">新嘟文</string> + <string name="action_login">登录 Mastodon 账号</string> + <string name="action_logout">退出登录</string> + <string name="action_logout_confirm">确定要退出账号 %1$s 吗?</string> + <string name="action_follow">关注</string> + <string name="action_unfollow">取消关注</string> + <string name="action_block">屏蔽</string> + <string name="action_unblock">取消屏蔽</string> + <string name="action_hide_reblogs">隐藏转嘟</string> + <string name="action_show_reblogs">显示转嘟</string> + <string name="action_report">报告</string> + <string name="action_delete">删除</string> + <string name="action_delete_and_redraft">删除并重新编辑</string> + <string name="action_send">嘟嘟</string> + <string name="action_send_public">嘟嘟!</string> + <string name="action_retry">重试</string> + <string name="action_close">关闭</string> + <string name="action_view_profile">个人资料</string> + <string name="action_view_preferences">设置</string> + <string name="action_view_account_preferences">账户设置</string> + <string name="action_view_favourites">喜欢</string> + <string name="action_view_mutes">被隐藏的用户</string> + <string name="action_view_blocks">被屏蔽的用户</string> + <string name="action_view_follow_requests">关注请求</string> + <string name="action_view_media">媒体</string> + <string name="action_open_in_web">在浏览器中打开</string> + <string name="action_add_media">从相册中选择</string> + <string name="action_photo_take">拍照</string> + <string name="action_share">分享</string> + <string name="action_mute">隐藏</string> + <string name="action_unmute">取消隐藏</string> + <string name="action_mention">提及</string> + <string name="action_hide_media">隐藏媒体文件</string> + <string name="action_open_drawer">打开应用抽屉</string> + <string name="action_save">保存</string> + <string name="action_edit_profile">编辑个人资料</string> + <string name="action_edit_own_profile">编辑</string> + <string name="action_undo">撤销</string> + <string name="action_accept">接受</string> + <string name="action_reject">拒绝</string> + <string name="action_search">搜索</string> + <string name="action_access_drafts">草稿</string> + <string name="action_toggle_visibility">设置嘟文可见范围</string> + <string name="action_content_warning">设置内容提醒信息</string> + <string name="action_emoji_keyboard">插入表情符号</string> + <string name="action_add_tab">添加标签页</string> + <string name="action_links">链接</string> + <string name="action_mentions">提及</string> + <string name="action_hashtags">话题</string> + <string name="action_open_reblogger">打开转嘟用户主页</string> + <string name="action_open_reblogged_by">显示转嘟</string> + <string name="action_open_faved_by">显示喜欢</string> + <string name="title_hashtags_dialog">话题</string> + <string name="title_mentions_dialog">提及</string> + <string name="title_links_dialog">链接</string> + <string name="action_open_media_n">打开媒体文件 #%1$d</string> + <string name="download_image">正在下载 %1$s…</string> + <string name="action_copy_link">复制链接</string> + <string name="action_open_as">打开为 %1$s</string> + <string name="action_share_as">分享为 …</string> + <string name="download_media">下载媒体文件</string> + <string name="downloading_media">正在下载媒体文件</string> + <string name="send_post_link_to">分享链接到…</string> + <string name="send_post_content_to">分享嘟文到…</string> + <string name="send_media_to">分享媒体到…</string> + <string name="confirmation_reported">报告已发送!</string> + <string name="confirmation_unblocked">已解除屏蔽</string> + <string name="confirmation_unmuted">已取消隐藏</string> + <string name="hint_domain">域名</string> + <string name="hint_compose">有什么新鲜事?</string> + <string name="hint_content_warning">设置内容提醒信息</string> + <string name="hint_display_name">昵称</string> + <string name="hint_note">简介</string> + <string name="hint_search">搜索…</string> + <string name="search_no_results">没找到结果</string> + <string name="label_quick_reply">回复…</string> + <string name="label_avatar">头像</string> + <string name="label_header">标题</string> + <string name="link_whats_an_instance">需要帮助?</string> + <string name="login_connection">正在连接…</string> + <string name="dialog_whats_an_instance">请输入你账号所在的 Mastodon 站点的域名,比如 pawoo.net,acg.mn,wxw.moe,<a href="https://instances.social">等等</a> 。 +\n +\n还没有 Mastodon 账号?你也可以输入想注册的 Mastodon 站点的域名,然后在该服务器创建新的账号并授权 Tusky 登入。 +\n +\n在 Mastodon 里,跨站互动和站内互动一样简单。可以前往 <a href="https://joinmastodon.org">https://joinmastodon.org</a> 了解更多信息。 </string> + <string name="dialog_title_finishing_media_upload">正在结束上传…</string> + <string name="dialog_message_uploading_media">正在上传…</string> + <string name="dialog_download_image">下载</string> + <string name="dialog_message_cancel_follow_request">移除关注请求?</string> + <string name="dialog_unfollow_warning">不再关注此用户?</string> + <string name="dialog_delete_post_warning">删除这条嘟文?</string> + <string name="dialog_redraft_post_warning">删除并重新编辑这条嘟文?</string> + <string name="visibility_public">公开:所有人可见,并会出现在公共时间轴上</string> + <string name="visibility_unlisted">不公开:所有人可见,但不会出现在公共时间轴上</string> + <string name="visibility_private">仅关注者:只有经过你确认后关注你的用户可见</string> + <string name="visibility_direct">私信:只有被提及的用户可见</string> + <string name="pref_title_edit_notification_settings">通知设置</string> + <string name="pref_title_notifications_enabled">通知</string> + <string name="pref_title_notification_alerts">提醒</string> + <string name="pref_title_notification_alert_sound">通知铃声</string> + <string name="pref_title_notification_alert_vibrate">振动</string> + <string name="pref_title_notification_alert_light">呼吸灯</string> + <string name="pref_title_notification_filters">事件</string> + <string name="pref_title_notification_filter_mentions">被提及</string> + <string name="pref_title_notification_filter_follows">有新的关注者</string> + <string name="pref_title_notification_filter_reblogs">嘟文被转嘟</string> + <string name="pref_title_notification_filter_favourites">嘟文被喜欢</string> + <string name="pref_title_notification_filter_poll">投票已结束</string> + <string name="pref_title_appearance_settings">外观</string> + <string name="pref_title_app_theme">应用主题</string> + <string name="pref_title_timelines">时间轴</string> + <string name="pref_title_timeline_filters">过滤器</string> + <string name="app_them_dark">黑夜</string> + <string name="app_theme_light">白天</string> + <string name="app_theme_black">暗色</string> + <string name="app_theme_auto">自动切换</string> + <string name="app_theme_system">跟随系统设定</string> + <string name="pref_title_browser_settings">浏览器</string> + <string name="pref_title_custom_tabs">使用 Chrome Custom Tabs</string> + <string name="pref_title_language">界面语言</string> + <string name="pref_title_post_filter">时间轴过滤</string> + <string name="pref_title_post_tabs">标签页</string> + <string name="pref_title_show_boosts">显示转嘟</string> + <string name="pref_title_show_replies">显示回复</string> + <string name="pref_title_show_media_preview">显示预览图</string> + <string name="pref_title_proxy_settings">代理</string> + <string name="pref_title_http_proxy_settings">HTTP 代理</string> + <string name="pref_title_http_proxy_enable">启用 HTTP 代理</string> + <string name="pref_title_http_proxy_server">HTTP 代理服务器</string> + <string name="pref_title_http_proxy_port">HTTP 代理端口</string> + <string name="pref_default_post_privacy">嘟文默认可见范围</string> + <string name="pref_default_media_sensitivity">自动标记媒体为敏感内容</string> + <string name="pref_publishing">发布(与服务器同步)</string> + <string name="pref_failed_to_sync">同步设置失败</string> + <string name="post_privacy_public">公开</string> + <string name="post_privacy_unlisted">不公开</string> + <string name="post_privacy_followers_only">仅关注者</string> + <string name="pref_post_text_size">字体大小</string> + <string name="post_text_size_smallest">最小</string> + <string name="post_text_size_small">小</string> + <string name="post_text_size_medium">标准</string> + <string name="post_text_size_large">大</string> + <string name="post_text_size_largest">最大</string> + <string name="notification_mention_name">提及</string> + <string name="notification_mention_descriptions">当有用户在嘟文中提及我时</string> + <string name="notification_follow_name">关注</string> + <string name="notification_follow_description">当有用户关注我时</string> + <string name="notification_boost_name">转嘟</string> + <string name="notification_boost_description">当有用户转嘟了我的嘟文时</string> + <string name="notification_favourite_name">喜欢</string> + <string name="notification_favourite_description">当有用户喜欢了我的嘟文时</string> + <string name="notification_poll_name">投票</string> + <string name="notification_poll_description">当我参与的投票结束时</string> + <string name="notification_mention_format">%1$s 提及了你</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s 和 %4$d 人</string> + <string name="notification_summary_medium">%1$s, %2$s, 和 %3$s</string> + <string name="notification_summary_small">%1$s 和 %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="other">%1$d 个新互动</item> + </plurals> + <string name="description_account_locked">锁嘟用户</string> + <string name="about_title_activity">关于 Tusky</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_tusky_license">Tusky 是基于 GNU General Public License Version 3 许可证开源的自由软件。完整的许可证协议:https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site"> + 项目地址:\n + https://tusky.app + </string> + <string name="about_bug_feature_request_site"> + 问题反馈:\n + https://github.com/tuskyapp/Tusky/issues + </string> + <string name="about_tusky_account">Tusky 官方账号</string> + <string name="post_share_content">分享嘟文内容</string> + <string name="post_share_link">分享嘟文链接</string> + <string name="post_media_images">照片</string> + <string name="post_media_video">视频</string> + <string name="state_follow_requested">已发送关注请求</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">%1$d 年内</string> + <string name="abbreviated_in_days">%1$d 天内</string> + <string name="abbreviated_in_hours">%1$d 小时内</string> + <string name="abbreviated_in_minutes">%1$d 分钟内</string> + <string name="abbreviated_in_seconds">%1$d 秒内</string> + <string name="abbreviated_years_ago">%1$d 年前</string> + <string name="abbreviated_days_ago">%1$d 天前</string> + <string name="abbreviated_hours_ago">%1$d 小时前</string> + <string name="abbreviated_minutes_ago">%1$d 分钟前</string> + <string name="abbreviated_seconds_ago">%1$d 秒前</string> + <string name="follows_you">关注了你</string> + <string name="pref_title_alway_show_sensitive_media">总是显示所有敏感媒体内容</string> + <string name="title_media">媒体</string> + <string name="replying_to">回复 @%1$s</string> + <string name="load_more_placeholder_text">加载更多</string> + <string name="pref_title_public_filter_keywords">公共时间轴</string> + <string name="pref_title_thread_filter_keywords">对话</string> + <string name="filter_addition_title">添加新的过滤器</string> + <string name="filter_edit_title">编辑过滤器</string> + <string name="filter_dialog_remove_button">移除</string> + <string name="filter_dialog_update_button">更新</string> + <string name="filter_add_description">需要过滤的文字</string> + <string name="add_account_name">添加账号</string> + <string name="add_account_description">添加新的 Mastodon 账号</string> + <string name="action_lists">列表</string> + <string name="title_lists">列表</string> + <string name="error_create_list">无法新建列表</string> + <string name="error_rename_list">无法重命名列表</string> + <string name="error_delete_list">无法删除列表</string> + <string name="action_create_list">新建列表</string> + <string name="action_rename_list">重命名列表</string> + <string name="action_delete_list">删除列表</string> + <string name="hint_search_people_list">搜索已关注的用户</string> + <string name="action_add_to_list">添加用户到列表</string> + <string name="action_remove_from_list">从列表中移除用户</string> + <string name="compose_active_account_description">以 %1$s 发布嘟文</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">为视觉障碍用户提供的描述\n(限制 %1$d 字)</item> + </plurals> + <string name="action_set_caption">设置图片标题</string> + <string name="action_remove">移除</string> + <string name="lock_account_label">保护你的账户(锁嘟)</string> + <string name="lock_account_label_description">你需要手动审核所有关注请求</string> + <string name="compose_save_draft">保存为草稿?</string> + <string name="send_post_notification_title">正在发送…</string> + <string name="send_post_notification_error_title">发送失败</string> + <string name="send_post_notification_channel_name">嘟文发送中</string> + <string name="send_post_notification_cancel_title">发送已被取消</string> + <string name="send_post_notification_saved_content">嘟文已保存为草稿</string> + <string name="action_compose_shortcut">新嘟文</string> + <string name="error_no_custom_emojis">当前实例 %1$s 没有自定义表情符号</string> + <string name="emoji_style">表情符号风格</string> + <string name="system_default">系统默认</string> + <string name="download_fonts">需要下载表情符号数据</string> + <string name="performing_lookup_title">正在查询…</string> + <string name="expand_collapse_all_posts">展开/折叠所有嘟文</string> + <string name="action_open_post">打开嘟文</string> + <string name="restart_required">需要重启应用</string> + <string name="restart_emoji">你需要重启 Tusky 才能生效</string> + <string name="later">稍后</string> + <string name="restart">立即重启</string> + <string name="caption_systememoji">系统内置的表情符号</string> + <string name="caption_blobmoji">Android 4.4–7.1 的黄馒头表情符号</string> + <string name="caption_twemoji">Mastodon 使用的表情符号</string> + <!-- string name="emoji_shortcode_format" translatable="false">:%1$s:</string --> + <string name="download_failed">下载失败</string> + <string name="profile_badge_bot_text">机器人</string> + <string name="account_moved_description">%1$s 已迁移到:</string> + <string name="reblog_private">转嘟(可见者不变)</string> + <string name="unreblog_private">取消转嘟</string> + <string name="license_description">Tusky 使用了以下开源项目的源码:</string> + <string name="license_apache_2">以 Apache License 授权(详见下方)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">个人资料附加信息</string> + <string name="profile_metadata_add">添加信息</string> + <string name="profile_metadata_label_label">标签</string> + <string name="profile_metadata_content_label">内容</string> + <string name="pref_title_absolute_time">嘟文显示精确时间</string> + <string name="label_remote_account">以下信息可能并不完整,要查看完整资料请使用浏览器打开</string> + <string name="unpin_action">取消置顶</string> + <string name="pin_action">置顶</string> + <plurals name="favs"> + <item quantity="other"><b>%1$s</b> 次喜欢</item> + </plurals> + <plurals name="reblogs"> + <item quantity="other"><b>%1$s</b> 次转嘟</item> + </plurals> + <string name="title_reblogged_by">转嘟</string> + <string name="title_favourited_by">喜欢</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s 和 %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s 和 %3$d 等人</string> + <string name="description_post_media"> + 媒体: %1$s + </string> + <string name="description_post_cw"> + 内容提醒: %1$s + </string> + <string name="description_post_media_no_description_placeholder"> + 没有媒体描述信息 + </string> + <string name="description_post_reblogged"> + 被转嘟 + </string> + <string name="description_post_favourited">被喜欢</string> + <string name="description_visibility_public"> + 公开 + </string> + <string name="description_visibility_unlisted"> + 不公开 + </string> + <string name="description_visibility_private">关注者</string> + <string name="description_visibility_direct"> + 私信 + </string> + <string name="hint_list_name">列表名</string> + <string name="edit_hashtag_hint">话题名(不含前面的 # 号)</string> + <string name="notifications_clear">清空</string> + <string name="notifications_apply_filter">分类</string> + <string name="filter_apply">应用</string> + <string name="compose_shortcut_long_label">发表新嘟文</string> + <string name="compose_shortcut_short_label">新嘟文</string> + <string name="pref_title_bot_overlay">显示机器人标志</string> + <string name="notification_clear_text">你确定要永久清空通知列表吗?</string> + <string name="poll_info_format"> + <!-- 15 votes • 1 hour left --> + %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="other">%1$s 次投票</item> + </plurals> + <string name="poll_info_time_absolute">%1$s 结束</string> + <string name="poll_info_closed">已结束</string> + <string name="poll_vote">投票</string> + <string name="poll_ended_voted">你参与的投票已结束</string> + <string name="poll_ended_created">你创建的投票已结束</string> + <!--These are for timestamps on polls --> + <plurals name="poll_timespan_days"> + <item quantity="other">剩余 %1$d 天</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="other">剩余 %1$d 小时</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="other">剩余 %1$d 分钟</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="other">剩余 %1$d 秒</item> + </plurals> + <string name="hashtags">话题</string> + <string name="action_edit">编辑</string> + <string name="edit_poll">编辑</string> + <string name="notification_follow_request_name">关注请求</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000..93c6e86 --- /dev/null +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,567 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="error_generic">應用程式出現異常。</string> + <string name="error_network">網絡請求出錯,請檢查互聯網連接並重試!</string> + <string name="error_empty">內容不能為空。</string> + <string name="error_invalid_domain">該域名無效</string> + <string name="error_failed_app_registration">無法通過該伺服器的身份驗證。如果此問題持續發生,請嘗試選單中的“在瀏覽器中登錄”。</string> + <string name="error_no_web_browser_found">沒有可用的瀏覽器。</string> + <string name="error_authorization_unknown">認證過程出現未知錯誤。如果此問題持續發生,請嘗試選單中的“在瀏覽器中登錄”。</string> + <string name="error_authorization_denied">授權被拒絕。</string> + <string name="error_retrieving_oauth_token">無法獲取登入資訊。</string> + <string name="error_compose_character_limit">嘟文太長了!</string> + <string name="error_media_upload_type">無法上傳此類型的檔案。</string> + <string name="error_media_upload_opening">此檔案無法開啟。</string> + <string name="error_media_upload_permission">需要授予 Tusky 讀取媒體檔案的權限。</string> + <string name="error_media_download_permission">需要授予 Tusky 寫入儲存空間的權限。</string> + <string name="error_media_upload_image_or_video">無法在嘟文中同時插入影片和圖片。</string> + <string name="error_media_upload_sending">媒體檔案上傳失敗。</string> + <string name="error_sender_account_gone">嘟文發送時出錯。</string> + <string name="title_home">主頁</string> + <string name="title_notifications">通知</string> + <string name="title_public_local">本站時間軸</string> + <string name="title_public_federated">跨站公開時間軸</string> + <string name="title_direct_messages">私信</string> + <string name="title_tab_preferences">標籤頁</string> + <string name="title_view_thread">嘟文</string> + <string name="title_posts">嘟文</string> + <string name="title_posts_with_replies">嘟文和回覆</string> + <string name="title_posts_pinned">已置頂</string> + <string name="title_follows">正在關注</string> + <string name="title_followers">關注者</string> + <string name="title_favourites">我的最愛</string> + <string name="title_mutes">被靜音的使用者</string> + <string name="title_blocks">被封鎖的使用者</string> + <string name="title_follow_requests">關注請求</string> + <string name="title_edit_profile">編輯個人資料</string> + <string name="title_drafts">草稿</string> + <string name="title_licenses">開源授權</string> + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s 轉嘟了</string> + <string name="post_sensitive_media_title">敏感內容</string> + <string name="post_media_hidden_title">已隱藏的照片或影片</string> + <string name="post_sensitive_media_directions">點一下顯示</string> + <string name="post_content_warning_show_more">顯示更多</string> + <string name="post_content_warning_show_less">摺疊內容</string> + <string name="post_content_show_more">展開</string> + <string name="post_content_show_less">摺疊</string> + <string name="message_empty">沒有內容。</string> + <string name="footer_empty">還沒有內容,向下拉動即可重新整理!</string> + <string name="notification_reblog_format">%1$s 轉嘟了你的嘟文</string> + <string name="notification_favourite_format">%1$s 最愛了你的嘟文</string> + <string name="notification_follow_format">%1$s 關注了你</string> + <string name="report_username_format">檢舉使用者 @%1$s 的濫用行為</string> + <string name="report_comment_hint">更多評論?</string> + <string name="action_quick_reply">快速回覆</string> + <string name="action_reply">回覆</string> + <string name="action_reblog">轉嘟</string> + <string name="action_unreblog">取消轉嘟</string> + <string name="action_favourite">最愛</string> + <string name="action_unfavourite">取消最愛</string> + <string name="action_more">更多</string> + <string name="action_compose">撰寫嘟文</string> + <string name="action_login">登入 Mastodon 帳號</string> + <string name="action_logout">登出</string> + <string name="action_logout_confirm">確定要登出 %1$s 嗎?</string> + <string name="action_follow">關注</string> + <string name="action_unfollow">取消關注</string> + <string name="action_block">封鎖</string> + <string name="action_unblock">取消封鎖</string> + <string name="action_hide_reblogs">隱藏轉嘟</string> + <string name="action_show_reblogs">顯示轉嘟</string> + <string name="action_report">檢舉</string> + <string name="action_delete">刪除</string> + <string name="action_delete_and_redraft">刪除並重新編輯</string> + <string name="action_send">嘟嘟</string> + <string name="action_send_public">嘟嘟!</string> + <string name="action_retry">重試</string> + <string name="action_close">關閉</string> + <string name="action_view_profile">個人資料</string> + <string name="action_view_preferences">設定</string> + <string name="action_view_account_preferences">帳戶設定</string> + <string name="action_view_favourites">我的最愛</string> + <string name="action_view_mutes">被靜音的使用者</string> + <string name="action_view_blocks">被封鎖的使用者</string> + <string name="action_view_follow_requests">關注請求</string> + <string name="action_view_media">媒體</string> + <string name="action_open_in_web">在瀏覽器中開啟</string> + <string name="action_add_media">從相簿中選擇</string> + <string name="action_photo_take">拍照</string> + <string name="action_share">分享</string> + <string name="action_mute">靜音</string> + <string name="action_unmute">取消靜音</string> + <string name="action_mention">提及</string> + <string name="action_hide_media">隱藏媒體檔案</string> + <string name="action_open_drawer">打開應用抽屜</string> + <string name="action_save">儲存</string> + <string name="action_edit_profile">編輯個人資料</string> + <string name="action_edit_own_profile">編輯</string> + <string name="action_undo">復原</string> + <string name="action_accept">接受</string> + <string name="action_reject">拒絕</string> + <string name="action_search">搜尋</string> + <string name="action_access_drafts">草稿</string> + <string name="action_toggle_visibility">設定嘟文可見範圍</string> + <string name="action_content_warning">敏感內容警告</string> + <string name="action_emoji_keyboard">插入表情符號</string> + <string name="action_add_tab">新增標籤頁</string> + <string name="action_links">連結</string> + <string name="action_mentions">提及</string> + <string name="action_hashtags">話題</string> + <string name="action_open_reblogger">打開轉嘟用戶主頁</string> + <string name="action_open_reblogged_by">顯示轉嘟</string> + <string name="action_open_faved_by">顯示最愛</string> + <string name="title_hashtags_dialog">話題</string> + <string name="title_mentions_dialog">提及</string> + <string name="title_links_dialog">連結</string> + <string name="action_open_media_n">打開媒體 #%1$d</string> + <string name="download_image">正在下載 %1$s</string> + <string name="action_copy_link">複製連結</string> + <string name="action_open_as">打開為 %1$s</string> + <string name="action_share_as">分享為 …</string> + <string name="download_media">下載媒體</string> + <string name="downloading_media">正在下載媒體</string> + <string name="send_post_link_to">分享連結到…</string> + <string name="send_post_content_to">分享嘟文到…</string> + <string name="send_media_to">分享媒體到…</string> + <string name="confirmation_reported">已發送!</string> + <string name="confirmation_unblocked">已解除封鎖</string> + <string name="confirmation_unmuted">已解除靜音</string> + <string name="hint_domain">哪一個域名?</string> + <string name="hint_compose">有什麼新鮮事?</string> + <string name="hint_content_warning">敏感內容警告</string> + <string name="hint_display_name">暱稱</string> + <string name="hint_note">簡介</string> + <string name="hint_search">搜尋…</string> + <string name="search_no_results">沒找到結果</string> + <string name="label_quick_reply">回覆…</string> + <string name="label_avatar">頭像</string> + <string name="label_header">標題</string> + <string name="link_whats_an_instance">什麼是站點?</string> + <string name="login_connection">正在連線…</string> + <string name="dialog_whats_an_instance">輸入你帳號所在的 Mastodon 站點的域名或地址,譬如 mastodon.social、icosahedron.website、social.tchncs.de 和 <a href="https://instances.social">更多</a> +\n +\n如果你還沒有帳號,你可以輸入你想要加入的域名並在此建立新帳號。 +\n +\n一個站點是一個託管你的帳號的地方,但是你可以很容易的跟不同站台的人們交流,就像是在同一個站台一樣。 +\n +\n更多資訊可以在 <a href="https://joinmastodon.org">joinmastodon.org</a> 查看。 </string> + <string name="dialog_title_finishing_media_upload">正在完成上傳</string> + <string name="dialog_message_uploading_media">正在上傳…</string> + <string name="dialog_download_image">下載</string> + <string name="dialog_message_cancel_follow_request">移除關注請求?</string> + <string name="dialog_unfollow_warning">停止關注此使用者?</string> + <string name="dialog_delete_post_warning">刪除這條嘟文?</string> + <string name="dialog_redraft_post_warning">刪除並重新編輯這條嘟文?</string> + <string name="visibility_public">公開:所有人可見,並會出現在公開時間軸上</string> + <string name="visibility_unlisted">不公開:所有人可見,但不會出現在公開時間軸上</string> + <string name="visibility_private">僅關注者:只有經過你確認後關注你的使用者可見</string> + <string name="visibility_direct">私信:只有被提及的使用者可見</string> + <string name="pref_title_edit_notification_settings">通知設定</string> + <string name="pref_title_notifications_enabled">通知</string> + <string name="pref_title_notification_alerts">提醒</string> + <string name="pref_title_notification_alert_sound">通知鈴聲</string> + <string name="pref_title_notification_alert_vibrate">振動</string> + <string name="pref_title_notification_alert_light">呼吸燈</string> + <string name="pref_title_notification_filters">事件</string> + <string name="pref_title_notification_filter_mentions">被提及</string> + <string name="pref_title_notification_filter_follows">有新的關注者</string> + <string name="pref_title_notification_filter_reblogs">嘟文被轉嘟</string> + <string name="pref_title_notification_filter_favourites">嘟文被加入最愛</string> + <string name="pref_title_notification_filter_poll">投票已結束</string> + <string name="pref_title_appearance_settings">外觀</string> + <string name="pref_title_app_theme">佈景主題</string> + <string name="pref_title_timelines">時間軸</string> + <string name="pref_title_timeline_filters">過濾器</string> + <string name="app_them_dark">黑夜</string> + <string name="app_theme_light">白天</string> + <string name="app_theme_black">暗色</string> + <string name="app_theme_auto">自動切換</string> + <string name="app_theme_system">跟隨系統</string> + <string name="pref_title_browser_settings">瀏覽器</string> + <string name="pref_title_custom_tabs">使用 Chrome Custom Tabs</string> + <string name="pref_title_language">語言</string> + <string name="pref_title_post_filter">時間軸過濾</string> + <string name="pref_title_post_tabs">標籤頁</string> + <string name="pref_title_show_boosts">顯示轉嘟</string> + <string name="pref_title_show_replies">顯示回覆</string> + <string name="pref_title_show_media_preview">顯示預覽圖</string> + <string name="pref_title_proxy_settings">代理伺服器</string> + <string name="pref_title_http_proxy_settings">HTTP 代理伺服器</string> + <string name="pref_title_http_proxy_enable">啟用 HTTP 代理伺服器</string> + <string name="pref_title_http_proxy_server">HTTP 代理伺服器</string> + <string name="pref_title_http_proxy_port">HTTP 代理伺服器埠</string> + <string name="pref_default_post_privacy">嘟文預設可見範圍</string> + <string name="pref_default_media_sensitivity">總是將媒體標示為敏感</string> + <string name="pref_publishing">發佈(與伺服器同步)</string> + <string name="pref_failed_to_sync">同步設定失敗</string> + <string name="post_privacy_public">公開</string> + <string name="post_privacy_unlisted">不公開</string> + <string name="post_privacy_followers_only">僅關注者</string> + <string name="pref_post_text_size">字體大小</string> + <string name="post_text_size_smallest">最小</string> + <string name="post_text_size_small">小</string> + <string name="post_text_size_medium">標準</string> + <string name="post_text_size_large">大</string> + <string name="post_text_size_largest">最大</string> + <string name="notification_mention_name">提及</string> + <string name="notification_mention_descriptions">當有使用者在嘟文中提及我時</string> + <string name="notification_follow_name">關注</string> + <string name="notification_follow_description">當有使用者關注我時</string> + <string name="notification_boost_name">轉嘟</string> + <string name="notification_boost_description">當有使用者轉嘟了我的嘟文時</string> + <string name="notification_favourite_name">最愛</string> + <string name="notification_favourite_description">當有使用者把我的嘟文加入最愛時</string> + <string name="notification_poll_name">投票</string> + <string name="notification_poll_description">當我參與的投票結束時</string> + <string name="notification_mention_format">%1$s 提及了你</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s 和 %4$d 人</string> + <string name="notification_summary_medium">%1$s, %2$s, 和 %3$s</string> + <string name="notification_summary_small">%1$s 和 %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="other">%1$d 個新互動</item> + </plurals> + <string name="description_account_locked">鎖嘟用戶</string> + <string name="about_title_activity">關於 Tusky</string> + <string name="about_tusky_license">Tusky 是基於 GNU General Public License Version 3 許可證開源的自由軟體完整的許可證協議:https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site"> + 專案網站:\n + https://tusky.app + </string> + <string name="about_bug_feature_request_site"> + 問題回報:\n + https://github.com/tuskyapp/Tusky/issues + </string> + <string name="about_tusky_account">Tusky 官方帳號</string> + <string name="post_share_content">分享嘟文內容</string> + <string name="post_share_link">分享嘟文連結</string> + <string name="post_media_images">照片</string> + <string name="post_media_video">影片</string> + <string name="state_follow_requested">已請求關注</string> + <!--These are for timestamps on statuses. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">%1$d 年內</string> + <string name="abbreviated_in_days">%1$d 天內</string> + <string name="abbreviated_in_hours">%1$d 小時內</string> + <string name="abbreviated_in_minutes">%1$d 分鐘內</string> + <string name="abbreviated_in_seconds">%1$d 秒內</string> + <string name="abbreviated_years_ago">%1$d 年前</string> + <string name="abbreviated_days_ago">%1$d 天前</string> + <string name="abbreviated_hours_ago">%1$d 小時前</string> + <string name="abbreviated_minutes_ago">%1$d 分鐘前</string> + <string name="abbreviated_seconds_ago">%1$d 秒前</string> + <string name="follows_you">關注了你</string> + <string name="pref_title_alway_show_sensitive_media">總是顯示所有敏感媒體內容</string> + <string name="title_media">媒體</string> + <string name="replying_to">回覆 @%1$s</string> + <string name="load_more_placeholder_text">載入更多</string> + <string name="pref_title_public_filter_keywords">公共時間軸</string> + <string name="pref_title_thread_filter_keywords">對話</string> + <string name="filter_addition_title">添加新的過濾器</string> + <string name="filter_edit_title">編輯過濾器</string> + <string name="filter_dialog_remove_button">移除</string> + <string name="filter_dialog_update_button">更新</string> + <string name="filter_add_description">需要過濾的文字</string> + <string name="add_account_name">加入帳號</string> + <string name="add_account_description">加入新的 Mastodon 帳號</string> + <string name="action_lists">列表</string> + <string name="title_lists">列表</string> + <string name="error_create_list">無法新建列表</string> + <string name="error_rename_list">無法重命名列表</string> + <string name="error_delete_list">無法刪除列表</string> + <string name="action_create_list">新建列表</string> + <string name="action_rename_list">重命名列表</string> + <string name="action_delete_list">刪除列表</string> + <string name="hint_search_people_list">搜索已關注的用戶</string> + <string name="action_add_to_list">添加用戶到列表</string> + <string name="action_remove_from_list">從列表中移除用戶</string> + <string name="compose_active_account_description">以 %1$s 發嘟文</string> + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">為視覺障礙用戶提供的描述 +\n(限制 %1$d 字)</item> + </plurals> + <string name="action_set_caption">設定圖片標題</string> + <string name="action_remove">移除</string> + <string name="lock_account_label">保護你的帳戶(鎖嘟)</string> + <string name="lock_account_label_description">你需要手動審核所有關注請求</string> + <string name="compose_save_draft">儲存為草稿?</string> + <string name="send_post_notification_title">正在發送…</string> + <string name="send_post_notification_error_title">發送失敗</string> + <string name="send_post_notification_channel_name">嘟文發送中</string> + <string name="send_post_notification_cancel_title">發送已被取消</string> + <string name="send_post_notification_saved_content">嘟文已儲存為草稿</string> + <string name="action_compose_shortcut">發表新嘟文</string> + <string name="error_no_custom_emojis">當前站點 %1$s 沒有自訂表情符號</string> + <string name="emoji_style">表情符號風格</string> + <string name="system_default">系統預設</string> + <string name="download_fonts">你需要先下載這些表情符號包</string> + <string name="performing_lookup_title">正在查詢…</string> + <string name="expand_collapse_all_posts">展開/摺疊所有嘟文</string> + <string name="action_open_post">開啟嘟文</string> + <string name="restart_required">需要重啟應用程式</string> + <string name="restart_emoji">你需要重啟 Tusky 才能生效</string> + <string name="later">稍後</string> + <string name="restart">立即重啟</string> + <string name="caption_systememoji">系統預設的表情符號包</string> + <string name="caption_blobmoji">Android 4.4–7.1 的黃饅頭表情符號</string> + <string name="caption_twemoji">Mastodon 使用的表情符號</string> + <!-- string name="emoji_shortcode_format" translatable="false">:%1$s:</string --> + <string name="download_failed">下載失敗</string> + <string name="profile_badge_bot_text">機器人</string> + <string name="account_moved_description">%1$s 已遷移到:</string> + <string name="reblog_private">轉嘟(可見者不變)</string> + <string name="unreblog_private">取消轉嘟</string> + <string name="license_description">Tusky 使用了以下開源專案的原始碼:</string> + <string name="license_apache_2">以 Apache License 授權(詳見下方)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + <string name="profile_metadata_label">個人資料附加資訊</string> + <string name="profile_metadata_add">新增資訊</string> + <string name="profile_metadata_label_label">標籤</string> + <string name="profile_metadata_content_label">內容</string> + <string name="pref_title_absolute_time">嘟文顯示精確時間</string> + <string name="label_remote_account">以下資訊可能並不完整,要檢視完整資料請使用瀏覽器開啟。</string> + <string name="unpin_action">取消置頂</string> + <string name="pin_action">置頂</string> + <plurals name="favs"> + <item quantity="other"><b>%1$s</b> 次最愛</item> + </plurals> + <plurals name="reblogs"> + <item quantity="other"><b>%1$s</b> 次轉嘟</item> + </plurals> + <string name="title_reblogged_by">轉嘟</string> + <string name="title_favourited_by">最愛由</string> + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s 和 %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s 和 %3$d 等人</string> + <string name="description_post_media"> + 媒體: %1$s + </string> + <string name="description_post_cw"> + 內容提醒: %1$s + </string> + <string name="description_post_media_no_description_placeholder"> + 沒有媒體描述信息 + </string> + <string name="description_post_reblogged"> + 被轉嘟 + </string> + <string name="description_post_favourited">被最愛</string> + <string name="description_visibility_public"> + 公開 + </string> + <string name="description_visibility_unlisted"> + 不公開 + </string> + <string name="description_visibility_private"> + 僅關注者 + </string> + <string name="description_visibility_direct"> + 私信 + </string> + <string name="hint_list_name">列表名</string> + <string name="edit_hashtag_hint">話題名(不含前面的 # 號)</string> + <string name="notifications_clear">清空</string> + <string name="notifications_apply_filter">分類</string> + <string name="filter_apply">應用</string> + <string name="compose_shortcut_long_label">發表新嘟文</string> + <string name="compose_shortcut_short_label">新嘟文</string> + <string name="pref_title_bot_overlay">顯示機器人標誌</string> + <string name="notification_clear_text">你確定要永久清空通知列表嗎?</string> + <string name="poll_info_format"> + <!-- 15 votes • 1 hour left --> + %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="other">%1$s 次投票</item> + </plurals> + <string name="poll_info_time_absolute">%1$s 結束</string> + <string name="poll_info_closed">已結束</string> + <string name="poll_vote">投票</string> + <string name="poll_ended_voted">你參與的投票已結束</string> + <string name="poll_ended_created">你創建的投票已結束</string> + <!--These are for timestamps on polls --> + <plurals name="poll_timespan_days"> + <item quantity="other">剩餘 %1$d 天</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="other">剩餘 %1$d 小時</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="other">剩餘 %1$d 分鐘</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="other">剩餘 %1$d 秒</item> + </plurals> + <string name="title_domain_mutes">隱藏域名</string> + <string name="action_view_domain_mutes">隱藏域名</string> + <string name="action_mute_domain">靜音 %1$s</string> + <string name="confirmation_domain_unmuted">%1$s 已解除靜音</string> + <string name="mute_domain_warning">你確定要封鎖 %1$s 域名嗎?您將不會在任何聯邦時間軸或通知中看到該域名中的內容,且來自該域名的關注者將被移除。</string> + <string name="mute_domain_warning_dialog_ok">隱藏整個域名</string> + <string name="pref_title_animate_gif_avatars">GIF 動畫大頭貼</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="filter_dialog_whole_word">整個單詞</string> + <string name="filter_dialog_whole_word_description">如果關鍵字或短語僅為字母或數字,則只有在匹配整個單詞時才會應用</string> + <string name="caption_notoemoji">Google 正在使用的 Emoji 符號集</string> + <string name="description_poll">使用以下選項創建投票:%1$s, %2$s, %3$s, %4$s; %5$s</string> + <string name="compose_preview_image_description">圖片 %1$s 的動作</string> + <string name="button_continue">繼續</string> + <string name="button_back">返回</string> + <string name="button_done">完成</string> + <string name="report_sent_success">成功回報 @%1$s</string> + <string name="hint_additional_info">附加留言</string> + <string name="report_remote_instance">轉發到 %1$s</string> + <string name="failed_report">回報失敗</string> + <string name="failed_fetch_posts">無法獲取狀態</string> + <string name="report_description_1">該報告將發送給您的伺服器管理員。 您可以在下面提供有關回報此帳戶的原因的說明:</string> + <string name="report_description_remote_instance">該帳戶來自其他伺服器。向那裡發送一份匿名的報告副本?</string> + <string name="hashtags">話題</string> + <string name="notification_follow_request_name">關注請求</string> + <string name="edit_poll">編輯</string> + <string name="action_edit">編輯</string> + <string name="title_bookmarks">書籤</string> + <string name="wellbeing_hide_stats_profile">隱藏個人頁面中的狀態數量資訊</string> + <string name="wellbeing_hide_stats_posts">隱藏貼文上的狀態數量資訊</string> + <string name="limit_notifications">限制時間軸通知</string> + <string name="review_notifications">檢查通知設定</string> + <string name="wellbeing_mode_notice">有些資訊可能會影響你的心理健康將會被隱藏。包括: +\n +\n- 最愛/轉嘟/關注 通知 +\n- 最愛/轉嘟 數量 +\n- 關注/貼文 在個人頁面的狀態 +\n +\n推播通知不會受到影響,但你可以手動檢查你的通知設定。</string> + <string name="pref_title_wellbeing_mode">數位健康</string> + <plurals name="poll_info_people"> + <item quantity="other">%1$s 人</item> + </plurals> + <string name="notification_subscription_format">%1$s 剛剛發了新嘟文</string> + <string name="notification_follow_request_format">%1$s 請求關注你</string> + <string name="pref_title_animate_custom_emojis">動態自訂表情符號</string> + <string name="drafts_post_reply_removed">你的草稿欲回覆的原嘟文已被刪除</string> + <string name="draft_deleted">草稿已刪除</string> + <string name="drafts_failed_loading_reply">載入回覆資訊失敗</string> + <string name="drafts_post_failed_to_send">這條嘟文發送失敗!</string> + <string name="post_media_attachments">附件</string> + <string name="post_media_audio">錄音</string> + <string name="dialog_delete_list_warning">你確定要刪除列表 %1$s?</string> + <string name="duration_7_days">7 天</string> + <string name="duration_3_days">3 天</string> + <string name="duration_1_day">1 天</string> + <string name="duration_6_hours">6 小時</string> + <string name="duration_1_hour">1 小時</string> + <string name="duration_30_min">30 分鐘</string> + <string name="duration_5_min">5 分鐘</string> + <string name="duration_indefinite">無限期</string> + <string name="label_duration">期間</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="other">你無法上傳超過 %1$d 媒體附件。</item> + </plurals> + <string name="notification_subscription_description">當你關注的人發布新嘟文時通知</string> + <string name="notification_subscription_name">新嘟文</string> + <string name="pref_title_notification_filter_subscriptions">我關注的人有新嘟文</string> + <string name="no_announcements">沒有公告。</string> + <string name="title_announcements">公告</string> + <string name="account_note_saved">已儲存!</string> + <string name="account_note_hint">你對此帳號的個人註記</string> + <string name="pref_title_hide_top_toolbar">隱藏頂端工具列的標題</string> + <string name="dialog_mute_hide_notifications">隱藏通知</string> + <string name="action_unmute_desc">取消靜音 %1$s</string> + <string name="action_unmute_domain">取消靜音 %1$s</string> + <string name="pref_main_nav_position_option_bottom">底端</string> + <string name="pref_main_nav_position_option_top">頂端</string> + <string name="pref_main_nav_position">主要導覽列的位置</string> + <string name="pref_title_gradient_for_media">在隱藏的媒體上使用漸變色彩</string> + <string name="add_hashtag_title">加上話題標籤</string> + <string name="pref_title_confirm_reblogs">在轉嘟時提示確認</string> + <string name="pref_title_show_cards_in_timelines">在時間軸中顯示連結預覽</string> + <string name="pref_title_enable_swipe_for_tabs">啟用在分頁間切換的滑動手勢</string> + <string name="notification_follow_request_description">關注請求的通知</string> + <string name="pref_title_notification_filter_follow_requests">已送出關注請求</string> + <string name="dialog_mute_warning">靜音 @%1$s?</string> + <string name="dialog_block_warning">封鎖 @%1$s?</string> + <string name="action_unmute_conversation">取消靜音對話</string> + <string name="action_mute_conversation">靜音對話</string> + <string name="warning_scheduling_interval">Mastodon 的最短發文間隔限制為 5 分鐘。</string> + <string name="no_drafts">你沒有任何草稿。</string> + <string name="no_scheduled_posts">你沒有任何已排程的嘟文。</string> + <string name="list">列表</string> + <string name="select_list_title">選擇列表</string> + <string name="description_post_bookmarked">被加入書籤</string> + <string name="action_view_bookmarks">我的書籤</string> + <string name="action_bookmark">書籤</string> + <string name="about_powered_by_tusky">由 Tusky 提供</string> + <string name="post_lookup_error_format">尋找嘟文時發生錯誤 %1$s</string> + <string name="action_reset_schedule">重設</string> + <string name="action_schedule_post">排程嘟文</string> + <string name="action_access_scheduled_posts">排程的嘟文</string> + <string name="title_scheduled_posts">已排程的嘟文</string> + <string name="poll_new_choice_hint">選項 %1$d</string> + <string name="poll_allow_multiple_choices">多個選項</string> + <string name="add_poll_choice">新增選項</string> + <string name="create_poll_title">投票</string> + <string name="action_add_poll">新增投票</string> + <string name="pref_title_alway_open_spoiler">總是顯示被標注為內容警告的嘟文</string> + <string name="failed_search">搜尋失敗</string> + <string name="title_accounts">帳號</string> + <string name="title_login">登入</string> + <string name="error_could_not_load_login_page">無法載入登入頁面。</string> + <string name="delete_scheduled_post_warning">確定要刪除這則排程嘟文嗎?</string> + <string name="instance_rule_info">登入既代表您已同意 %1$s 的規定。</string> + <string name="instance_rule_title">%1$s 的規定</string> + <string name="dialog_delete_conversation_warning">確認要刪除此對話嗎?</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + <string name="set_focus_description">輕按或拖動圓圈來選擇總是在縮圖中可視的關注點。</string> + <string name="description_post_language">嘟文語言</string> + <string name="compose_save_draft_loses_media">是否要儲存草稿?(當你重開草稿時附檔將會被再次上傳。)</string> + <string name="tusky_compose_post_quicksetting_label">編寫嘟文</string> + <string name="failed_to_pin">釘選失敗</string> + <string name="failed_to_unpin">取消釘選失敗</string> + <string name="pref_title_show_self_username">在工具列顯示使用者名稱</string> + <string name="pref_title_confirm_favourites">標註為喜歡前顯示確認對話框</string> + <string name="account_date_joined">加入自 %1$s</string> + <string name="pref_show_self_username_always">總是</string> + <string name="pref_show_self_username_disambiguate">登入多個帳號時</string> + <string name="pref_show_self_username_never">從不</string> + <string name="notification_sign_up_name">註冊</string> + <string name="notification_sign_up_description">新使用者通知</string> + <string name="notification_update_name">嘟文編輯</string> + <string name="notification_update_description">當你互動過的嘟文被編輯時發出通知</string> + <string name="follow_requests_info">雖然您的帳號未上鎖,管理者 %1$s 認為您或許需要手動處理來自這些帳號的追蹤請求。</string> + <string name="action_subscribe_account">訂閱</string> + <string name="action_unsubscribe_account">取消訂閱</string> + <string name="saving_draft">正在儲存草稿…</string> + <string name="tips_push_notification_migration">重新登入所有帳號以啟用推播功能。</string> + <string name="action_set_focus">設置關注點</string> + <string name="title_migration_relogin">重新登入以啟用推播功能</string> + <string name="error_multimedia_size_limit">影片和音訊檔案大小不能超過 %1$s MB。</string> + <string name="status_count_one_plus">1+</string> + <string name="action_add_reaction">添加反應</string> + <string name="pref_title_notification_filter_sign_ups">有人進行了註冊</string> + <string name="action_edit_image">編輯圖片</string> + <string name="duration_30_days">30 天</string> + <string name="duration_60_days">60 天</string> + <string name="duration_90_days">90 天</string> + <string name="duration_180_days">180 天</string> + <string name="duration_365_days">365 天</string> + <string name="duration_no_change">(無更改)</string> + <string name="error_following_hashtag_format">追蹤 #%1$s 時發生錯誤</string> + <string name="error_unfollowing_hashtag_format">取消追蹤 #%1$s 時發生錯誤</string> + <string name="action_unbookmark">移除書籤</string> + <string name="action_delete_conversation">刪除對話</string> + <string name="action_dismiss">撤銷</string> + <string name="action_details">詳情</string> + <string name="pref_title_notification_filter_updates">我互動過的嘟文被編輯了</string> + <string name="duration_14_days">14 天</string> + <string name="dialog_push_notification_migration">為了透過 UnifiedPush使用推播功能,Tusky 需要獲得訂閱您 Mastodon 服務器上的通知之權限。這會需要重新登入才能更改授予 Tusky 的 OAuth 範疇。在此頁面或帳戶設定頁面中使用重新登入選項將會保留您所有的本機草稿和快取。</string> + <string name="dialog_push_notification_migration_other_accounts">您已重新登入當前帳號並授予 Tusky 推送訂閱的權限。 然而,您仍擁有其他帳號未以此種方式遷移。 請切換到該帳號,並且逐一重新登入,以啟用 UnifiedPush 的通知支援。</string> + <string name="error_loading_account_details">加載賬戶詳情失敗</string> + <string name="notification_sign_up_format">%1$s 已註冊</string> + <string name="notification_update_format">%1$s 編輯了他們的嘟文</string> + <string name="error_image_edit_failed">這張圖片不能編輯。</string> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values/actions.xml b/app/src/main/res/values/actions.xml new file mode 100644 index 0000000..f76bfb7 --- /dev/null +++ b/app/src/main/res/values/actions.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <item name="action_expand_collapse_cw" type="id" /> + <item name="action_reply" type="id" /> + <item name="action_favourite" type="id" /> + <item name="action_unfavourite" type="id" /> + <item name="action_bookmark" type="id" /> + <item name="action_unbookmark" type="id" /> + <item name="action_reblog" type="id" /> + <item name="action_unreblog" type="id" /> + <item name="action_open_profile" type="id" /> + <item name="action_open_media_1" type="id" /> + <item name="action_open_media_2" type="id" /> + <item name="action_open_media_3" type="id" /> + <item name="action_open_media_4" type="id" /> + <item name="action_open_mention" type="id" /> + <item name="action_expand_cw" type="id" /> + <item name="action_collapse_cw" type="id" /> + <item name="action_links" type="id" /> + <item name="action_mentions" type="id" /> + <item name="action_hashtags" type="id" /> + <item name="action_open_reblogger" type="id" /> + <item name="action_open_reblogged_by" type="id" /> + <item name="action_open_faved_by" type="id" /> + <item name="action_more" type="id" /> + <item name="action_untranslate" type="id" /> +</resources> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..5cd9a21 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <declare-styleable name="ClickableSpanTextView"> + <!-- Necessary to support tools:text in Android Studio layout designer --> + <attr name="android:text"/> + </declare-styleable> + + <declare-styleable name="LicenseCard"> + <attr name="name" format="string|reference" /> + <attr name="license" format="string|reference" /> + <attr name="link" format="string|reference" /> + <attr name="description" format="string|reference" /> + </declare-styleable> + + <declare-styleable name="GraphView"> + <attr name="primaryLineColor" format="reference|color" /> + <attr name="secondaryLineColor" format="reference|color" /> + <attr name="lineWidth" format="dimension" /> + <attr name="graphColor" format="reference|color" /> + <attr name="metaColor" format="reference|color" /> + <attr name="proportionalTrending" format="boolean" /> + </declare-styleable> + + <declare-styleable name="SliderPreference"> + <attr name="android:value" format="string|reference" /> + <attr name="android:valueFrom" format="string|reference" /> + <attr name="android:valueTo" format="string|reference" /> + <attr name="android:stepSize" format="string|reference" /> + <attr name="format" format="string|reference" /> + <attr name="iconStart" format="reference" /> + <attr name="iconEnd" format="reference" /> + </declare-styleable> + + <!--Themed Attributes--> + <attr name="colorBackgroundAccent" format="reference|color" /> + <attr name="colorBackgroundHighlight" format="reference|color" /> + <attr name="textColorDisabled" format="reference|color" /> + <attr name="iconColor" format="reference|color" /> + <attr name="windowBackgroundColor" format="reference|color" /> + <attr name="dividerColor" format="reference|color" /> + + <attr name="status_text_small" format="dimension" /> + <attr name="status_text_medium" format="dimension" /> + <attr name="status_text_large" format="dimension" /> + +</resources> diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..4ab130a --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,53 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <!-- the original Tusky Blue, but for better contrast tusky_blue_light and tusky_blue_dark should be used in the ui--> + <color name="tusky_blue">#2b90d9</color> + <color name="tusky_blue_light">#3c9add</color> + <color name="tusky_blue_dark">#217aba</color> + <color name="tusky_blue_lighter">#56a7e1</color> + <color name="tusky_orange">#ca8f04</color> + <color name="tusky_orange_light">#fab207</color> + <color name="tusky_green">#00731B</color> + <color name="tusky_green_light">#25d069</color> + <color name="tusky_green_lighter">#CCFFD8</color> + <color name="tusky_red">#DF1553</color> + <color name="tusky_red_lighter">#FF7287</color> + + <color name="white">#fff</color> + <color name="black">#000</color> + + <color name="notification_color">@color/tusky_blue</color> + + <!-- the number roughly corresponds to the % lightness of the grey --> + <color name="tusky_grey_05">#070b14</color> + <color name="tusky_grey_10">#16191f</color> + <color name="tusky_grey_15">#21222c</color> + <color name="tusky_grey_20">#282c37</color> + <color name="tusky_grey_25">#313543</color> + <color name="tusky_grey_30">#444b5d</color> + <color name="tusky_grey_40">#596378</color> + <color name="tusky_grey_50">#6e7b92</color> + <color name="tusky_grey_70">#9baec8</color> + <color name="tusky_grey_80">#b9c8d8</color> + <color name="tusky_grey_90">#d9e1e8</color> + <color name="tusky_grey_95">#ebeff4</color> + + <color name="transparent_tusky_blue">#8c2b90d9</color> + <color name="transparent_black">#8f000000</color> + <color name="header_background_filter_dark">#44000000</color> + <color name="header_background_filter_light">#66FFFFFF</color> + <color name="transparent_statusbar_background">#44000000</color> + + <!-- colors used in the elephant friend drawables --> + <color name="elephant_friend_border_color">#121419</color> + <color name="elephant_friend_accent_color">?attr/colorPrimary</color> + <color name="elephant_friend_body_color">#9BAEC8</color> + <color name="elephant_friend_dark_body_color_1">#8192A6</color> + <color name="elephant_friend_dark_body_color_2">#7F90A4</color> + <color name="elephant_friend_light_color_1">#CAD4E0</color> + <color name="elephant_friend_light_color_2">#d9e1e8</color> + + <!-- colors used in the app icon --> + <color name="icon_background">#09497b</color> + <color name="icon_highlight">#39acff</color> +</resources> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..4c52609 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,84 @@ +<resources> + <dimen name="status_display_name_padding_end">4dp</dimen> + <dimen name="status_reblogged_bar_padding_top">8dp</dimen> + <dimen name="status_media_preview_margin_top">8dp</dimen> + <dimen name="status_media_preview_height">100dp</dimen> + <dimen name="status_detail_media_preview_height">130dp</dimen> + <dimen name="compose_media_preview_margin">8dp</dimen> + <dimen name="compose_media_preview_margin_bottom">0dp</dimen> + <dimen name="compose_media_preview_size">120dp</dimen> + <dimen name="account_avatar_margin">14dp</dimen> + <dimen name="tab_page_margin">16dp</dimen> + <dimen name="status_line_margin_start">36dp</dimen> + <dimen name="text_content_margin">16dp</dimen> + <dimen name="status_sensitive_media_button_padding">5dp</dimen> + + <dimen name="card_image_vertical_height">210dp</dimen> + <dimen name="card_image_horizontal_width">100dp</dimen> + + <dimen name="compose_activity_snackbar_elevation">16dp</dimen> + + <dimen name="account_activity_scroll_title_visible_height">200dp</dimen> + <dimen name="account_activity_avatar_size">100dp</dimen> + + <dimen name="compose_activity_scrollview_height">-1px</dimen> <!-- match_parent --> + <dimen name="timeline_width">-1px</dimen> <!-- match_parent --> + + <dimen name="actionbar_elevation">4dp</dimen> + + <item name="wrap_content" format="integer" type="dimen">-2</item> + + <dimen name="preference_icon_size">20dp</dimen> + + <dimen name="selected_drag_item_elevation">12dp</dimen> + + <dimen name="avatar_radius_94dp">11.75dp</dimen> <!-- 1/8 of 100dp - 2 * 3dp padding --> + <dimen name="avatar_radius_80dp">10dp</dimen> <!-- 1/8 of 80dp --> + <dimen name="avatar_radius_48dp">6dp</dimen> <!-- 1/8 of 48dp --> + <dimen name="avatar_radius_42dp">5.25dp</dimen> <!-- 1/8 of 42dp --> + <dimen name="avatar_radius_36dp">4.5dp</dimen> <!-- 1/8 of 36dp --> + <dimen name="avatar_radius_24dp">3dp</dimen> <!-- 1/8 of 24dp --> + <dimen name="min_report_button_width">160dp</dimen> + <dimen name="account_avatar_background_radius">14dp</dimen> + + + <dimen name="card_radius">5dp</dimen> + + <dimen name="poll_preview_padding">12dp</dimen> + <dimen name="poll_preview_min_width">120dp</dimen> + + <dimen name="adaptive_bitmap_inner_size">74dp</dimen> + <dimen name="adaptive_bitmap_outer_size">108dp</dimen> + + <dimen name="fabMargin">16dp</dimen> + + <dimen name="avatar_toolbar_nav_icon_size">36dp</dimen> + + <dimen name="profile_media_spacing">3dp</dimen> + + <dimen name="profile_media_audio_icon_padding">16dp</dimen> + + <dimen name="preview_image_spacing">4dp</dimen> + + <dimen name="graph_line_thickness">1dp</dimen> + + <dimen name="minimum_touch_target">48dp</dimen> + + <dimen name="timeline_status_avatar_height">48dp</dimen> + <dimen name="timeline_status_avatar_width">48dp</dimen> + + <dimen name="profile_badge_stroke_width">1dp</dimen> + <dimen name="profile_badge_min_height">24dp</dimen> + <dimen name="profile_badge_icon_size">16dp</dimen> + <dimen name="profile_badge_icon_start_padding">8dp</dimen> + <dimen name="profile_badge_icon_end_padding">0dp</dimen> + + <dimen name="account_swiperefresh_distance">64dp</dimen> + + <dimen name="fallback_emoji_size">16sp</dimen> + + <!-- 56dp + 16dp padding on top and bottom --> + <dimen name="recyclerview_bottom_padding_actionbutton">88dp</dimen> + <dimen name="recyclerview_bottom_padding_no_actionbutton">32dp</dimen> + +</resources> diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml new file mode 100644 index 0000000..237180e --- /dev/null +++ b/app/src/main/res/values/donottranslate.xml @@ -0,0 +1,262 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="tusky_website" translatable="false">https://tusky.app</string> + <string name="about_app_version">%1$s %2$s</string> + + <string name="oauth_scheme" translatable="false">oauth2redirect</string> + <string name="preferences_file_key" translatable="false">com.keylesspalace.tusky.PREFERENCES</string> + + <string name="at_symbol" translatable="false">\@</string> + <string name="hash_symbol" translatable="false">#</string> + <string name="title_tag" translatable="false">#%1$s</string> + + <string name="emoji_shortcode_format" translatable="false">:%1$s:</string> + <string name="post_timestamp_with_edited_indicator" translatable="false">%1$s *</string> + <string name="metadata_joiner" translatable="false">" • "</string> + <string name="date_range" translatable="false">%1$s — %2$s</string> + + <string-array name="post_privacy_values"> + <item>public</item> + <item>unlisted</item> + <item>private</item> + </string-array> + + <string-array name="post_text_size_values"> + <item>smallest</item> + <item>small</item> + <item>medium</item> + <item>large</item> + <item>largest</item> + </string-array> + + <string-array name="language_entries"> + <item>@string/system_default</item> + <item>Català</item> + <item>Čeština</item> + <item>Cymraeg</item> + <item>Deutsch</item> + <item>English (UK)</item> + <item>English (US)</item> + <item>Esperanto</item> + <item>Español</item> + <item>Euskara</item> + <item>Français</item> + <item>Gaeilge</item> + <item>Gàidhlig</item> + <item>Galego</item> + <item>íslenska</item> + <item>Italiano</item> + <item>Magyar</item> + <item>Nederlands</item> + <item>Norsk</item> + <item>Occitan</item> + <item>Polski</item> + <item>Português (Brasil)</item> + <item>Português (Portugal)</item> + <item>Slovenščina</item> + <item>Svenska</item> + <item>Taqbaylit</item> + <item>Tiếng Việt</item> + <item>Türkçe</item> + <item>Беларуская</item> + <item>български</item> + <item>Русский</item> + <item>Українська</item> + <item>العربية</item> + <item>کوردیی ناوەندی</item> + <item>বাংলা (বাংলাদেশ)</item> + <item>বাংলা (ভারত)</item> + <item>فارسی</item> + <item>हिंदी</item> + <item>संस्कृतम्</item> + <item>ଓଡ଼ିଆ</item> <!-- or: Odia --> + <item>தமிழ்</item> + <item>ภาษาไทย</item> + <item>한국어</item> + <item>中文(台灣)</item> + <item>中文(新加坡)</item> + <item>中文(澳門)</item> + <item>中文(简体)</item> + <item>中文(香港)</item> + <item>日本語</item> + </string-array> + + <string-array name="language_values"> + <item>default</item> + <item>ca</item> + <item>cs</item> + <item>cy</item> + <item>de</item> + <item>en-GB</item> + <item>en</item> + <item>eo</item> + <item>es</item> + <item>eu</item> + <item>fr</item> + <item>ga</item> + <item>gd</item> + <item>gl</item> + <item>is</item> + <item>it</item> + <item>hu</item> + <item>nl</item> + <item>nb-NO</item> + <item>oc</item> + <item>pl</item> + <item>pt-BR</item> + <item>pt-PT</item> + <item>sl</item> + <item>sv</item> + <item>kab</item> + <item>vi</item> + <item>tr</item> + <item>be</item> + <item>bg</item> + <item>ru</item> + <item>uk</item> + <item>ar</item> + <item>ckb</item> + <item>bn-BD</item> + <item>bn-IN</item> + <item>fa</item> + <item>hi</item> + <item>sa</item> + <item>or</item> + <item>ta</item> + <item>th</item> + <item>ko</item> + <item>zh-TW</item> + <item>zh-SG</item> + <item>zh-MO</item> + <item>zh-CN</item> + <item>zh-HK</item> + <item>ja</item> + </string-array> + + <string-array name="pref_main_nav_position_options"> + <item>@string/pref_main_nav_position_option_top</item> + <item>@string/pref_main_nav_position_option_bottom</item> + </string-array> + + <string-array name="pref_main_nav_position_values"> + <item>top</item> + <item>bottom</item> + </string-array> + + <string-array name="pref_show_self_username_values"> + <item>always</item> + <item>disambiguate</item> + <item>never</item> + </string-array> + + <string name="description_status" translatable="false"> + <!-- + %1$s|display_name %2$s|CW?; %3$s|content?, %15$s|poll?, %4$s|date, + %6$s|reposted_by?; %7$s|username, %5$s|edited?, %16$s|translated?, %8$s|reposted?, %9$s|favorited?, + %10$s|bookmarked?, %11$s|media?; %12$s|visibility, %13$s|fav_number?, + %14$s|reblog_number? + --> + %1$s; %2$s; %3$s, %15$s, %4$s, %6$s; %7$s, %5$s, %16$s, %8$s, %9$s, %10$s, %11$s; %12$s, %13$s, %14$s + </string> + + <string-array name="rick_roll_domains" translatable="false"> + <item>gab.com</item> + <item>gab.ai</item> + <item>spinster.xyz</item> + <item>truthsocial.com</item> + </string-array> + + <string name="rick_roll_url">https://www.youtube.com/watch?v=dQw4w9WgXcQ</string> + + + <string-array name="poll_duration_names"> + <item>@string/duration_5_min</item> + <item>@string/duration_30_min</item> + <item>@string/duration_1_hour</item> + <item>@string/duration_6_hours</item> + <item>@string/duration_1_day</item> + <item>@string/duration_3_days</item> + <item>@string/duration_7_days</item> + <item>@string/duration_14_days</item> + <item>@string/duration_30_days</item> + <item>@string/duration_60_days</item> + <item>@string/duration_90_days</item> + <item>@string/duration_180_days</item> + <item>@string/duration_365_days</item> + </string-array> + + <integer-array name="poll_duration_values"> <!-- values in seconds, corresponding to poll_duration_names --> + <item>300</item> + <item>1800</item> + <item>3600</item> + <item>21600</item> + <item>86400</item> + <item>259200</item> + <item>604800</item> + <item>1209600</item> + <item>2592000</item> + <item>5184000</item> + <item>7776000</item> + <item>15552000</item> + <item>31536000</item> + </integer-array> + + <string name="poll_percent_format"><!-- 15% --> <b>%1$d%%</b></string> + + <string-array name="mute_duration_names"> + <item>@string/duration_indefinite</item> + <item>@string/duration_5_min</item> + <item>@string/duration_30_min</item> + <item>@string/duration_1_hour</item> + <item>@string/duration_6_hours</item> + <item>@string/duration_1_day</item> + <item>@string/duration_3_days</item> + <item>@string/duration_7_days</item> + </string-array> + + <integer-array name="mute_duration_values"> <!-- values in seconds, corresponding to mute_duration_names --> + <item>0</item> + <item>300</item> + <item>1800</item> + <item>3600</item> + <item>21600</item> + <item>86400</item> + <item>259200</item> + <item>604800</item> + </integer-array> + + <string-array name="filter_duration_names"> + <item>@string/duration_indefinite</item> + <item>@string/duration_5_min</item> + <item>@string/duration_30_min</item> + <item>@string/duration_1_hour</item> + <item>@string/duration_6_hours</item> + <item>@string/duration_1_day</item> + <item>@string/duration_3_days</item> + <item>@string/duration_7_days</item> + </string-array> + + <integer-array name="filter_duration_values"> <!-- values in seconds, corresponding to mute_duration_names --> + <item>0</item> + <item>300</item> + <item>1800</item> + <item>3600</item> + <item>21600</item> + <item>86400</item> + <item>259200</item> + <item>604800</item> + </integer-array> + + <string-array name="reading_order_names"> + <item>@string/pref_reading_order_oldest_first</item> + <item>@string/pref_reading_order_newest_first</item> + </string-array> + + <string-array name="reading_order_values"> + <item>OLDEST_FIRST</item> + <item>NEWEST_FIRST</item> + </string-array> + + <string name="url_domain_notifier" translatable="false">%1$s (🔗 %2$s)</string> + +</resources> diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml new file mode 100644 index 0000000..ba6866c --- /dev/null +++ b/app/src/main/res/values/ids.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <item name="pin" type="id" /> + <item name="custom_emoji_targets_tag" type="id" /> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml new file mode 100644 index 0000000..e85c561 --- /dev/null +++ b/app/src/main/res/values/integers.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <integer name="profile_media_column_count">3</integer> + + <integer name="trending_column_count">1</integer> +</resources> diff --git a/app/src/main/res/values/plurals.xml b/app/src/main/res/values/plurals.xml new file mode 100644 index 0000000..43d56fc --- /dev/null +++ b/app/src/main/res/values/plurals.xml @@ -0,0 +1,6 @@ +<resources> + <plurals name="action_post_failed_detail"> + <item quantity="one">@string/action_post_failed_detail</item> + <item quantity="other">@string/action_post_failed_detail_plural</item> + </plurals> +</resources> \ No newline at end of file diff --git a/app/src/main/res/values/string-arrays.xml b/app/src/main/res/values/string-arrays.xml new file mode 100644 index 0000000..59e9976 --- /dev/null +++ b/app/src/main/res/values/string-arrays.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <string-array name="app_theme_names"> + <item>@string/app_them_dark</item> + <item>@string/app_theme_light</item> + <item>@string/app_theme_black</item> + <item>@string/app_theme_auto</item> + <item>@string/app_theme_system</item> + <item>@string/app_theme_system_black</item> + </string-array> + + <string-array name="post_privacy_names"> + <item>@string/post_privacy_public</item> + <item>@string/post_privacy_unlisted</item> + <item>@string/post_privacy_followers_only</item> + </string-array> + + <string-array name="post_text_size_names"> + <item>@string/post_text_size_smallest</item> + <item>@string/post_text_size_small</item> + <item>@string/post_text_size_medium</item> + <item>@string/post_text_size_large</item> + <item>@string/post_text_size_largest</item> + </string-array> + + <string-array name="pref_show_self_username_names"> + <item>@string/pref_show_self_username_always</item> + <item>@string/pref_show_self_username_disambiguate</item> + <item>@string/pref_show_self_username_never</item> + </string-array> + + <string-array name="filter_contexts"> + <item>@string/title_home</item> + <item>@string/title_notifications</item> + <item>@string/pref_title_public_filter_keywords</item> + <item>@string/pref_title_thread_filter_keywords</item> + <item>@string/pref_title_account_filter_keywords</item> + </string-array> + + <string-array name="filter_actions"> + <item>@string/filter_action_warn</item> + <item>@string/filter_action_hide</item> + </string-array> + + <!-- Order must be synchronized with MastoList.ReplyPolicy --> + <string-array name="list_reply_policies_display"> + <item>@string/list_reply_policy_none</item> + <item>@string/list_reply_policy_list</item> + <item>@string/list_reply_policy_followed</item> + </string-array> +</resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..cd9388f --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,864 @@ +<!-- + ~ Copyright 2023 Tusky Contributors + ~ + ~ This file is a part of Tusky. + ~ + ~ This program is free software; you can redistribute it and/or modify it under the terms of the + ~ GNU General Public License as published by the Free Software Foundation; either version 3 of the + ~ License, or (at your option) any later version. + ~ + ~ Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + ~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + ~ Public License for more details. + ~ + ~ You should have received a copy of the GNU General Public License along with Tusky; if not, + ~ see <http://www.gnu.org/licenses>. + --> + +<resources> + + <string name="error_generic">An error occurred.</string> + <string name="error_network">A network error occurred. Please check your connection and try again.</string> + <string name="error_empty">This cannot be empty.</string> + <string name="error_invalid_domain">Invalid domain entered</string> + <string name="error_failed_app_registration">Failed authenticating with that instance. If this persists, try "Login in Browser" from the menu.</string> + <string name="error_no_web_browser_found">Couldn\'t find a web browser to use.</string> + <string name="error_authorization_unknown">An unidentified authorization error occurred. If this persists, try "Login in Browser" from the menu.</string> + <string name="error_authorization_denied">Authorization was denied. If you\'re sure that you supplied the correct credentials, try "Login in Browser" from the menu.</string> + <string name="error_retrieving_oauth_token">Failed getting a login token. If this persists, try "Login in Browser" from the menu.</string> + <string name="error_loading_account_details">Failed loading account details</string> + <string name="error_could_not_load_login_page">Could not load the login page.</string> + <string name="error_compose_character_limit">The post is too long!</string> + <string name="error_multimedia_size_limit">Video and audio files cannot exceed %1$s MB in size.</string> + + <string name="error_image_edit_failed">The image could not be edited.</string> + <string name="error_media_upload_type">That type of file cannot be uploaded.</string> + <string name="error_media_upload_opening">That file could not be opened.</string> + <string name="error_media_upload_permission">Permission to read media is required.</string> + <string name="error_media_download_permission">Permission to store media is required.</string> + <string name="error_media_upload_image_or_video">Images and videos cannot both be attached to the same post.</string> + <string name="error_media_upload_sending">The upload failed.</string> + <string name="error_media_upload_sending_fmt">The upload failed: %1$s</string> + <string name="error_sender_account_gone">Error sending post.</string> + <string name="error_following_hashtag_format">Error following #%1$s</string> + <string name="error_unfollowing_hashtag_format">Error unfollowing #%1$s</string> + <string name="error_following_hashtags_unsupported">This instance does not support following hashtags.</string> + <string name="error_muting_hashtag_format">Error muting #%1$s</string> + <string name="error_unmuting_hashtag_format">Error unmuting #%1$s</string> + <string name="error_blocking_domain">Failed to mute %1$s: %2$s</string> + <string name="error_unblocking_domain">Failed to unmute %1$s: %2$s</string> + <string name="error_status_source_load">Failed to load the status source from the server.</string> + <string name="error_deleting_filter">Error deleting filter \'%1$s\'</string> + <string name="error_saving_filter">Error saving filter \'%1$s\'</string> + + <string name="title_login">Login</string> + <string name="title_home">Home</string> + <string name="title_notifications">Notifications</string> + <string name="title_public_local">Local</string> + <string name="title_public_trending_hashtags">Trending hashtags</string> + <string name="title_public_trending_statuses">Trending posts</string> + <string name="title_public_federated">Federated</string> + <string name="title_direct_messages">Direct messages</string> + <string name="title_tab_preferences">Tabs</string> + <string name="title_view_thread">Thread</string> + <string name="title_posts">Posts</string> + <string name="title_posts_with_replies">With replies</string> + <string name="title_posts_pinned">Pinned</string> + <string name="title_follows">Follows</string> + <string name="title_followers">Followers</string> + <string name="title_favourites">Favorites</string> + <string name="title_bookmarks">Bookmarks</string> + <string name="title_mutes">Muted users</string> + <string name="title_blocks">Blocked users</string> + <string name="title_domain_mutes">Hidden domains</string> + <string name="title_migration_relogin">Re-login for push notifications</string> + <string name="title_follow_requests">Follow requests</string> + <string name="title_edit_profile">Edit your profile</string> + <string name="title_drafts">Drafts</string> + <string name="title_scheduled_posts">Scheduled posts</string> + <string name="title_announcements">Announcements</string> + <string name="title_licenses">Licenses</string> + <string name="title_followed_hashtags">Followed hashtags</string> + <string name="title_edits">Edits</string> + + <string name="dialog_follow_hashtag_title">Follow hashtag</string> + <string name="dialog_follow_hashtag_hint">#hashtag</string> + + <string name="post_username_format">\@%1$s</string> + <string name="post_boosted_format">%1$s boosted</string> + <string name="post_sensitive_media_title">Sensitive content</string> + <string name="post_media_hidden_title">Media hidden</string> + <string name="post_media_alt">ALT</string> + <string name="post_sensitive_media_directions">Click to view</string> + <string name="post_content_warning_show_more">Show More</string> + <string name="post_content_warning_show_less">Show Less</string> + <string name="post_content_show_more">Expand</string> + <string name="post_content_show_less">Collapse</string> + <string name="post_edited">Edited %1$s</string> + + <string name="message_empty">Nothing here.</string> + <string name="footer_empty">Nothing here. Pull down to refresh!</string> + + <string name="notification_reblog_format">%1$s boosted your post</string> + <string name="notification_favourite_format">%1$s favorited your post</string> + <string name="notification_follow_format">%1$s followed you</string> + <string name="notification_follow_request_format">%1$s requested to follow you</string> + <string name="notification_sign_up_format">%1$s signed up</string> + <string name="notification_subscription_format">%1$s just posted</string> + <string name="notification_update_format">%1$s edited their post</string> + <string name="notification_report_format">New report on %1$s</string> + <string name="notification_header_report_format">%1$s reported %2$s</string> + <string name="notification_summary_report_format">%1$s · %2$d posts attached</string> + + <string name="report_username_format">Report @%1$s</string> + <string name="report_comment_hint">Additional comments?</string> + + <string name="action_quick_reply">Quick Reply</string> + <string name="action_reply">Reply</string> + <string name="action_reblog">Boost</string> + <string name="action_unreblog">Remove boost</string> + <string name="action_favourite">Favorite</string> + <string name="action_unfavourite">Remove favorite</string> + <string name="action_bookmark">Bookmark</string> + <string name="action_unbookmark">Remove bookmark</string> + <string name="action_more">More</string> + <string name="action_compose">Compose</string> + <string name="action_login">Login with Tusky</string> + <string name="action_browser_login">Login with Browser</string> + <string name="action_logout">Log out</string> + <string name="action_logout_confirm">Are you sure you want to log out of %1$s? This will delete all local data of the account, including drafts and preferences.</string> + <string name="action_post_failed">Upload failed</string> + <string name="action_post_failed_detail">Your post failed to upload and has been saved to drafts.\n\nEither the server could not be contacted, or it rejected the post.</string> + <string name="action_post_failed_detail_plural">Your posts failed to upload and have been saved to drafts.\n\nEither the server could not be contacted, or it rejected the posts.</string> + <string name="action_post_failed_show_drafts">Show drafts</string> + <string name="action_post_failed_do_nothing">Dismiss</string> + <string name="action_follow">Follow</string> + <string name="action_unfollow">Unfollow</string> + <string name="action_block">Block</string> + <string name="action_unblock">Unblock</string> + <string name="action_share_account_link">Share link to account</string> + <string name="action_share_account_username">Share username of account</string> + <string name="action_hide_reblogs">Hide boosts</string> + <string name="action_show_reblogs">Show boosts</string> + <string name="pref_title_show_self_boosts">Show self-boosts</string> + <string name="pref_title_show_self_boosts_description">Someone boosting their own post</string> + <string name="action_report">Report</string> + <string name="action_edit">Edit</string> + <string name="action_delete">Delete</string> + <string name="action_delete_conversation">Delete conversation</string> + <string name="action_delete_and_redraft">Delete and re-draft</string> + <string name="action_discard">Discard changes</string> + <string name="action_continue_edit">Continue editing</string> + <string name="action_send">TOOT</string> + <string name="action_send_public">TOOT!</string> + <string name="action_retry">Retry</string> + <string name="action_close">Close</string> + <string name="action_view_profile">Profile</string> + <string name="action_view_preferences">Preferences</string> + <string name="action_view_account_preferences">Account preferences</string> + <string name="action_view_favourites">Favorites</string> + <string name="action_view_bookmarks">Bookmarks</string> + <string name="action_view_mutes">Muted users</string> + <string name="action_view_blocks">Blocked users</string> + <string name="action_view_domain_mutes">Hidden domains</string> + <string name="action_view_follow_requests">Follow requests</string> + <string name="action_view_media">Media</string> + <string name="action_open_in_web">Open in browser</string> + <string name="action_add_media">Add media</string> + <string name="action_add_poll">Add poll</string> + <string name="action_photo_take">Take photo</string> + <string name="action_share">Share</string> + <string name="action_mute">Mute</string> + <string name="action_unmute">Unmute</string> + <string name="action_unmute_desc">Unmute %1$s</string> + <string name="action_mute_domain">Mute %1$s</string> + <string name="action_unmute_domain">Unmute %1$s</string> + <string name="action_mute_conversation">Mute conversation</string> + <string name="action_unmute_conversation">Unmute conversation</string> + <string name="action_mention">Mention</string> + <string name="action_hide_media">Hide media</string> + <string name="action_open_drawer">Open drawer</string> + <string name="action_save">Save</string> + <string name="action_edit_profile">Edit profile</string> + <string name="action_edit_own_profile">Edit</string> + <string name="action_undo">Undo</string> + <string name="action_accept">Accept</string> + <string name="action_reject">Reject</string> + <string name="action_search">Search</string> + <string name="action_access_drafts">Drafts</string> + <string name="action_access_scheduled_posts">Scheduled posts</string> + <string name="action_toggle_visibility">Post visibility</string> + <string name="action_content_warning">Content warning</string> + <string name="action_emoji_keyboard">Emoji keyboard</string> + <string name="action_schedule_post">Schedule Post</string> + <string name="action_reset_schedule">Reset</string> + <string name="action_add_tab">Add Tab</string> + <string name="action_links">Links</string> + <string name="action_mentions">Mentions</string> + <string name="action_hashtags">Hashtags</string> + <string name="action_follow_hashtag">Follow a new hashtag</string> + <string name="action_open_reblogger">Open boost author</string> + <string name="action_open_reblogged_by">Show boosts</string> + <string name="action_open_faved_by">Show favorites</string> + <string name="action_dismiss">Dismiss</string> + <string name="action_details">Details</string> + <string name="action_add_reaction">add reaction</string> + + <string name="title_hashtags_dialog">Hashtags</string> + <string name="title_mentions_dialog">Mentions</string> + <string name="title_links_dialog">Links</string> + <string name="action_open_media_n">Open media #%1$d</string> + + <string name="download_image">Downloading %1$s</string> + + <string name="action_copy_link">Copy the link</string> + <string name="action_open_as">Open as %1$s</string> + <string name="action_share_as">Share as …</string> + <string name="action_translate">Translate</string> + <string name="action_show_original">Show original</string> + <string name="download_media">Download media</string> + <string name="downloading_media">Downloading media</string> + + <string name="send_post_link_to">Share post URL to…</string> + <string name="send_post_content_to">Share post to…</string> + <string name="send_account_link_to">Share account URL to…</string> + <string name="send_account_username_to">Share account username to…</string> + <string name="send_media_to">Share media to…</string> + <string name="account_username_copied">Username copied</string> + <string name="url_copied">Url copied</string> + + <string name="confirmation_reported">Sent!</string> + <string name="confirmation_unblocked">User unblocked</string> + <string name="confirmation_unmuted">User unmuted</string> + <string name="confirmation_domain_unmuted">%1$s unhidden</string> + <string name="confirmation_hashtag_unfollowed">#%1$s unfollowed</string> + <string name="confirmation_hashtag_copied">\'#%1$s\' copied</string> + + <string name="reply_sending">Sending…</string> + <string name="reply_sending_long">Your reply is being sent.</string> + + <string name="hint_domain">Which instance?</string> + <string name="hint_compose">What\'s happening?</string> + <string name="hint_content_warning">Content warning</string> + <string name="hint_display_name">Display name</string> + <string name="hint_note">Bio</string> + <string name="hint_search">Search…</string> + <string name="hint_media_description_missing">Media should have a description.</string> + + <string name="search_no_results">No results</string> + + <string name="label_quick_reply">Reply…</string> + <string name="label_avatar">Avatar</string> + <string name="label_header">Header</string> + <string name="label_image">Image</string> + + <string name="link_whats_an_instance">What\'s an instance?</string> + + <string name="login_connection">Connecting…</string> + + <string name="dialog_whats_an_instance">The address or domain of any instance can be entered + here, such as mastodon.social, icosahedron.website, social.tchncs.de, and + <a href="https://instances.social">more!</a> + \n\nIf you don\'t yet have an account, you can enter the name of the instance you\'d like to + join and create an account there.\n\nAn instance is a single place where your account is + hosted, but you can easily communicate with and follow folks on other instances as though + you were on the same site. + \n\nMore info can be found at <a href="https://joinmastodon.org">joinmastodon.org</a>. + </string> + <string name="dialog_title_finishing_media_upload">Finishing Media Upload</string> + <string name="dialog_message_uploading_media">Uploading…</string> + <string name="dialog_download_image">Download</string> + <string name="dialog_message_cancel_follow_request">Revoke the follow request?</string> + <string name="dialog_unfollow_warning">Unfollow this account?</string> + <string name="dialog_follow_warning">Follow this account?</string> + <string name="dialog_delete_post_warning">Delete this post?</string> + <string name="dialog_redraft_post_warning">Delete and re-draft this post?</string> + <string name="dialog_delete_conversation_warning">Delete this conversation?</string> + <string name="mute_domain_warning">Are you sure you want to block all of %1$s? You will not see content from that domain in any public timelines or in your notifications. Your followers from that domain will be removed.</string> + <string name="mute_domain_warning_dialog_ok">Hide entire domain</string> + <string name="dialog_block_warning">Block @%1$s?</string> + <string name="dialog_mute_warning">Mute @%1$s?</string> + <string name="dialog_mute_hide_notifications">Hide notifications</string> + + <string name="visibility_public">Public: Post to public timelines</string> + <string name="visibility_unlisted">Unlisted: Do not show in public timelines</string> + <string name="visibility_private">Followers-Only: Post to followers only</string> + <string name="visibility_direct">Direct: Post to mentioned users only</string> + + <string name="pref_title_edit_notification_settings">Notifications</string> + <string name="pref_title_notifications_enabled">Notifications</string> + <string name="pref_title_notification_alerts">Alerts</string> + <string name="pref_title_notification_alert_sound">Notify with a sound</string> + <string name="pref_title_notification_alert_vibrate">Notify with vibration</string> + <string name="pref_title_notification_alert_light">Notify with light</string> + <string name="pref_title_notification_filters">Notify me when</string> + <string name="pref_title_notification_filter_mentions">mentioned</string> + <string name="pref_title_notification_filter_follows">followed</string> + <string name="pref_title_notification_filter_follow_requests">follow requested</string> + <string name="pref_title_notification_filter_reblogs">my posts are boosted</string> + <string name="pref_title_notification_filter_favourites">my posts are favorited</string> + <string name="pref_title_notification_filter_poll">polls have ended</string> + <string name="pref_title_notification_filter_subscriptions">somebody I\'m subscribed to published a new post</string> + <string name="pref_title_notification_filter_sign_ups">somebody signed up</string> + <string name="pref_title_notification_filter_updates">a post I\'ve interacted with is edited</string> + <string name="pref_title_notification_filter_reports">there\'s a new report</string> + <string name="pref_title_appearance_settings">Appearance</string> + <string name="pref_title_app_theme">App theme</string> + <string name="pref_title_timelines">Timelines</string> + <string name="pref_title_timeline_filters">Filters</string> + <string name="pref_title_per_timeline_preferences">Per-timeline preferences</string> + + <string name="app_them_dark">Dark</string> + <string name="app_theme_light">Light</string> + <string name="app_theme_black">Black</string> + <string name="app_theme_auto">Automatic at sunset</string> + <string name="app_theme_system">Use System Design</string> + <string name="app_theme_system_black">Use System Design (black)</string> + + <string name="pref_title_browser_settings">Browser</string> + <string name="pref_title_custom_tabs">Use Chrome Custom Tabs</string> + <string name="pref_title_language">Language</string> + <string name="pref_title_bot_overlay">Show indicator for bots</string> + <string name="pref_title_animate_gif_avatars">Animate GIF avatars</string> + <string name="pref_title_gradient_for_media">Show colorful gradients for hidden media</string> + <string name="pref_title_animate_custom_emojis">Animate custom emojis</string> + + <string name="pref_title_post_filter">Timeline filtering</string> + <string name="pref_title_post_tabs">Home timeline</string> + <string name="pref_title_show_boosts">Show boosts</string> + <string name="pref_title_show_replies">Show replies</string> + <string name="pref_title_show_media_preview">Download media previews</string> + <string name="pref_title_proxy_settings">Proxy</string> + <string name="pref_title_http_proxy_settings">HTTP proxy</string> + <string name="pref_title_http_proxy_enable">Enable HTTP proxy</string> + <string name="pref_title_http_proxy_server">HTTP proxy server</string> + <string name="pref_title_http_proxy_port">HTTP proxy port</string> + <string name="pref_title_http_proxy_port_message">Port should be between %1$d and %2$d</string> + <string name="pref_summary_http_proxy_disabled">Disabled</string> + <string name="pref_summary_http_proxy_missing"><not set></string> + <string name="pref_summary_http_proxy_invalid"><invalid></string> + + <string name="pref_default_post_privacy">Default post privacy (synced with server)</string> + <string name="pref_default_post_language">Default posting language (synced with server)</string> + <string name="pref_default_reply_privacy">Default reply privacy (not synced with server)</string> + <string name="pref_default_media_sensitivity">Always mark media as sensitive (synced with server)</string> + <string name="pref_publishing">Publishing</string> + <string name="pref_failed_to_sync">Failed to sync preferences</string> + + <string name="pref_main_nav_position">Main navigation position</string> + <string name="pref_main_nav_position_option_top">Top</string> + <string name="pref_main_nav_position_option_bottom">Bottom</string> + + + <string name="post_privacy_public">Public</string> + <string name="post_privacy_unlisted">Unlisted</string> + <string name="post_privacy_followers_only">Followers-only</string> + + <string name="pref_ui_text_size">UI text size</string> + <string name="pref_post_text_size">Post text size</string> + + <string name="post_text_size_smallest">Smallest</string> + <string name="post_text_size_small">Small</string> + <string name="post_text_size_medium">Medium</string> + <string name="post_text_size_large">Large</string> + <string name="post_text_size_largest">Largest</string> + + <string name="pref_show_self_username_always">Always</string> + <string name="pref_show_self_username_disambiguate">When multiple accounts logged in</string> + <string name="pref_show_self_username_never">Never</string> + + <string name="notification_mention_name">New mentions</string> + <string name="notification_mention_descriptions">Notifications about new mentions</string> + <string name="notification_follow_name">New followers</string> + <string name="notification_follow_description">Notifications about new followers</string> + <string name="notification_follow_request_name">Follow requests</string> + <string name="notification_follow_request_description">Notifications about follow requests</string> + <string name="notification_boost_name">Boosts</string> + <string name="notification_boost_description">Notifications when your posts get boosted</string> + <string name="notification_favourite_name">Favorites</string> + <string name="notification_favourite_description">Notifications when your posts get marked as favorite</string> + <string name="notification_poll_name">Polls</string> + <string name="notification_poll_description">Notifications about polls that have ended</string> + <string name="notification_subscription_name">New posts</string> + <string name="notification_subscription_description">Notifications when somebody you\'re subscribed to published a new post</string> + <string name="notification_sign_up_name">Sign ups</string> + <string name="notification_sign_up_description">Notifications about new users</string> + <string name="notification_update_name">Post edits</string> + <string name="notification_update_description">Notifications when posts you\'ve interacted with are edited</string> + <string name="notification_report_name">Reports</string> + <string name="notification_report_description">Notifications about moderation reports</string> + <string name="notification_listenable_worker_name">Background activity</string> + <string name="notification_listenable_worker_description">Notifications when Tusky is working in the background</string> + <string name="notification_unknown_name">Unknown</string> + + <string name="notification_mention_format">%1$s mentioned you</string> + <string name="notification_summary_large">%1$s, %2$s, %3$s and %4$d others</string> + <string name="notification_summary_medium">%1$s, %2$s, and %3$s</string> + <string name="notification_summary_small">%1$s and %2$s</string> + <plurals name="notification_title_summary"> + <item quantity="one">%1$d new interaction</item> + <item quantity="other">%1$d new interactions</item> + </plurals> + <string name="notification_notification_worker">Fetching notifications…</string> + <string name="notification_prune_cache">Cache maintenance…</string> + + <string name="description_account_locked">Locked Account</string> + + <string name="about_title_activity">About</string> + <string name="about_tusky_version">Tusky %1$s</string> + <string name="about_device_info_title">Your device</string> + <string name="about_device_info">%1$s %2$s\nAndroid version: %3$s\nSDK version: %4$d</string> + <string name="about_account_info_title">Your account</string> + <string name="about_account_info">\@%1$s\@%2$s\nVersion: %3$s</string> + <string name="about_powered_by_tusky">Powered by Tusky</string> + <string name="about_tusky_license">Tusky is free and open-source software. + It is licensed under the GNU General Public License Version 3. + You can view the license here: https://www.gnu.org/licenses/gpl-3.0.en.html</string> + <!-- note to translators: + * you should think of “free” as in “free speech,” not as in “free beer”. + We sometimes call it “libre software,” borrowing the French or Spanish word for “free” as in freedom, + to show we do not mean the software is gratis. Source: https://www.gnu.org/philosophy/free-sw.html + * the url can be changed to link to the localized version of the license. + --> + <string name="about_project_site">Project website: https://tusky.app</string> + <string name="about_bug_feature_request_site">Bug reports & feature requests:\nhttps://github.com/tuskyapp/Tusky/issues</string> + <string name="about_tusky_account">Tusky\'s Profile</string> + + <string name="post_share_content">Share content of post</string> + <string name="post_share_link">Share link to post</string> + <string name="post_media_image">Image</string> + <string name="post_media_images">Images</string> + <string name="post_media_video">Video</string> + <string name="post_media_audio">Audio</string> + <string name="post_media_attachments">Attachments</string> + <string name="status_count_one_plus">1+</string> + <string name="status_created_at_now">now</string> + <string name="status_filtered_show_anyway">Show anyway</string> + <string name="status_filter_placeholder_label_format">Filtered: %1$s</string> + + <string name="state_follow_requested">Follow requested</string> + + <!--These are for timestamps on posts. For example: "16s" or "2d"--> + <string name="abbreviated_in_years">in %1$dy</string> + <string name="abbreviated_in_days">in %1$dd</string> + <string name="abbreviated_in_hours">in %1$dh</string> + <string name="abbreviated_in_minutes">in %1$dm</string> + <string name="abbreviated_in_seconds">in %1$ds</string> + <string name="abbreviated_years_ago">%1$dy</string> + <string name="abbreviated_days_ago">%1$dd</string> + <string name="abbreviated_hours_ago">%1$dh</string> + <string name="abbreviated_minutes_ago">%1$dm</string> + <string name="abbreviated_seconds_ago">%1$ds</string> + + <string name="follows_you">Follows you</string> + <string name="pref_title_alway_show_sensitive_media">Always show sensitive content</string> + <string name="pref_title_alway_open_spoiler">Always expand posts marked with content warnings</string> + <string name="title_media">Media</string> + <string name="replying_to">Replying to @%1$s</string> + <string name="load_more_placeholder_text">load more</string> + + <string name="pref_title_public_filter_keywords">Public timelines</string> + <string name="pref_title_thread_filter_keywords">Conversations</string> + <string name="pref_title_account_filter_keywords">Profiles</string> + <string name="filter_addition_title">Add filter</string> + <string name="filter_edit_title">Edit filter</string> + <string name="filter_dialog_remove_button">Remove</string> + <string name="filter_dialog_update_button">Update</string> + <string name="filter_dialog_whole_word">Whole word</string> + <string name="filter_dialog_whole_word_description">When the keyword or phrase is alphanumeric only, it will only be applied if it matches the whole word</string> + <string name="filter_add_description">Phrase to filter</string> + <string name="filter_expiration_format">%1$s (%2$s)</string> + + <string name="add_account_name">Add Account</string> + <string name="add_account_description">Add new Mastodon Account</string> + + <string name="action_lists">Lists</string> + <string name="title_lists">Lists</string> + <string name="error_create_list">Could not create list</string> + <string name="error_rename_list">Could not update list</string> + <string name="error_delete_list">Could not delete list</string> + <string name="action_create_list">Create a list</string> + <string name="action_rename_list">Update the list</string> + <string name="action_delete_list">Delete the list</string> + <string name="hint_search_people_list">Search for people you follow</string> + <string name="action_add_to_list">Add account to the list</string> + <string name="action_remove_from_list">Remove account from the list</string> + <string name="action_add_or_remove_from_list">Add or remove from list</string> + <string name="failed_to_add_to_list">Failed to add the account to the list</string> + <string name="failed_to_remove_from_list">Failed to remove the account from the list</string> + + <string name="compose_active_account_description">Posting as %1$s</string> + + <plurals name="hint_describe_for_visually_impaired"> + <item quantity="other">Describe contents for visually impaired (%1$d character limit)</item> + </plurals> + <string name="set_focus_description">Tap or drag the circle to choose the focal point which will always be visible in thumbnails.</string> + <string name="action_set_caption">Set caption</string> + <string name="action_set_focus">Set focus point</string> + <string name="action_edit_image">Edit image</string> + <string name="action_remove">Remove</string> + <string name="lock_account_label">Lock account</string> + <string name="lock_account_label_description">Requires you to manually approve followers</string> + <string name="compose_delete_draft">Delete draft?</string> + <string name="compose_save_draft">Save draft?</string> + <string name="compose_save_draft_loses_media">Save draft? (Attachments will be uploaded again when you restore the draft.)</string> + <string name="compose_unsaved_changes">You have unsaved changes.</string> + <string name="send_post_notification_title">Sending post…</string> + <string name="send_post_notification_error_title">Error sending post</string> + <string name="send_post_notification_channel_name">Sending Posts</string> + <string name="send_post_notification_cancel_title">Sending cancelled</string> + <string name="send_post_notification_saved_content">A copy of the post has been saved to your drafts</string> + <string name="action_compose_shortcut">Compose</string> + + <string name="error_no_custom_emojis">Your instance %1$s does not have any custom emojis</string> + <string name="emoji_style">Emoji style</string> + <string name="system_default">System default</string> + <string name="download_fonts">You\'ll need to download these emoji sets first</string> + <string name="performing_lookup_title">Performing lookup…</string> + <string name="expand_collapse_all_posts">Expand/Collapse all posts</string> + <string name="action_open_post">Open post</string> + <string name="restart_required">App restart required</string> + <string name="restart_emoji">You\'ll need to restart Tusky in order to apply these changes</string> + <string name="later">Later</string> + <string name="restart">Restart</string> + <string name="caption_systememoji">Your device\'s default emoji set</string> + <string name="caption_blobmoji">The Blob emojis known from Android 4.4–7.1</string> + <string name="caption_twemoji">Mastodon\'s standard emoji set</string> + <string name="caption_notoemoji">Google\'s current emoji set</string> + + <string name="download_failed">Download failed</string> + + <string name="profile_badge_bot_text">Bot</string> + <string name="account_moved_description">%1$s has moved to:</string> + + <string name="reblog_private">Boost to original audience</string> + <string name="unreblog_private">Unboost</string> + + <string name="license_description">Tusky contains code and assets from the following open source projects:</string> + <string name="license_apache_2">Licensed under the Apache License (copy below)</string> + <string name="license_cc_by_4">CC-BY 4.0</string> + <string name="license_cc_by_sa_4">CC-BY-SA 4.0</string> + + <string name="profile_metadata_label">Profile metadata</string> + <string name="profile_metadata_add">add data</string> + <string name="profile_metadata_label_label">Label</string> + <string name="profile_metadata_content_label">Content</string> + + <string name="pref_title_absolute_time">Use absolute time</string> + + <string name="label_remote_account">Information below may reflect the user\'s profile incompletely. Press to open full profile in browser.</string> + + <string name="unpin_action">Unpin</string> + <string name="pin_action">Pin</string> + <string name="failed_to_pin">Failed to Pin</string> + <string name="failed_to_unpin">Failed to Unpin</string> + + <plurals name="favs"> + <item quantity="zero"><b>%1$s</b> Favorites</item> + <item quantity="one"><b>%1$s</b> Favorite</item> + <item quantity="other"><b>%1$s</b> Favorites</item> + </plurals> + + <plurals name="reblogs"> + <item quantity="zero"><b>%1$s</b> Boosts</item> + <item quantity="one"><b>%1$s</b> Boost</item> + <item quantity="other"><b>%1$s</b> Boosts</item> + </plurals> + + <string name="label_translating">Translating…</string> + <string name="label_translated">Translated from %1$s with %2$s</string> + + <string name="title_reblogged_by">Boosted by</string> + <string name="title_favourited_by">Favorited by</string> + + <string name="conversation_1_recipients">%1$s</string> + <string name="conversation_2_recipients">%1$s and %2$s</string> + <string name="conversation_more_recipients">%1$s, %2$s and %3$d more</string> + + <string name="description_post_media"> + Media: %1$s + </string> + <string name="description_post_cw"> + Content warning: %1$s + </string> + <string name="description_post_media_no_description_placeholder"> + No description + </string> + <string name="description_post_edited"> + Edited + </string> + <string name="description_post_reblogged"> + Reblogged + </string> + <string name="description_post_favourited"> + Favorited + </string> + <string name="description_post_bookmarked"> + Bookmarked + </string> + <string name="description_visibility_public"> + Public + </string> + <string name="description_visibility_unlisted"> + Unlisted + </string> + <string name="description_visibility_private"> + Followers + </string> + <string name="description_visibility_direct"> + Direct + </string> + <string name="description_poll"> + Poll with choices: %1$s, %2$s, %3$s, %4$s; %5$s + </string> + <string name="description_post_language">Post language</string> + <string name="description_login">Works in most cases. No data is leaked to other apps.</string> + <string name="description_browser_login">May support additional authentication methods, but requires a supported browser.</string> + + <string name="hint_list_name">List name</string> + + <string name="add_hashtag_title">Add hashtag</string> + <string name="edit_hashtag_hint">Hashtag without #</string> + <string name="hashtags">Hashtags</string> + <string name="select_list_title">Select list</string> + <string name="select_list_manage">Manage lists</string> + <string name="list_exclusive_label">Hide from the home timeline</string> + <string name="list">List</string> + <string name="notifications_clear">Delete</string> + <string name="notifications_apply_filter">Filter</string> + <string name="filter_apply">Apply</string> + + <string name="muting_hashtag_success_format">Muting hashtag #%1$s as a warning</string> + <string name="unmuting_hashtag_success_format">Unmuting hashtag #%1$s</string> + <string name="action_view_filter">View filter</string> + <string name="following_hashtag_success_format">Now following hashtag #%1$s</string> + <string name="unfollowing_hashtag_success_format">No longer following hashtag #%1$s</string> + + <string name="compose_shortcut_long_label">Compose post</string> + <string name="compose_shortcut_short_label">Compose</string> + + <string name="notification_clear_text">Are you sure you want to permanently clear all your notifications?</string> + <string name="compose_preview_image_description">Actions for image %1$s</string> + + <string name="poll_info_format"> + <!-- 15 votes • 1 hour left --> + %1$s • %2$s</string> + <plurals name="poll_info_votes"> + <item quantity="one">%1$s vote</item> + <item quantity="other">%1$s votes</item> + </plurals> + <plurals name="poll_info_people"> + <item quantity="one">%1$s person</item> + <item quantity="other">%1$s people</item> + </plurals> + <string name="poll_info_time_absolute">ends at %1$s</string> + <string name="poll_info_closed">closed</string> + + <string name="poll_vote">Vote</string> + + <string name="poll_ended_voted">A poll you have voted in has ended</string> + <string name="poll_ended_created">A poll you created has ended</string> + + <!--These are for timestamps on polls --> + <plurals name="poll_timespan_days"> + <item quantity="one">%1$d day left</item> + <item quantity="other">%1$d days left</item> + </plurals> + <plurals name="poll_timespan_hours"> + <item quantity="one">%1$d hour left</item> + <item quantity="other">%1$d hours left</item> + </plurals> + <plurals name="poll_timespan_minutes"> + <item quantity="one">%1$d minute left</item> + <item quantity="other">%1$d minutes left</item> + </plurals> + <plurals name="poll_timespan_seconds"> + <item quantity="one">%1$d second left</item> + <item quantity="other">%1$d seconds left</item> + </plurals> + + <string name="button_continue">Continue</string> + <string name="button_back">Back</string> + <string name="button_done">Done</string> + <string name="report_sent_success">Successfully reported @%1$s</string> + <string name="hint_additional_info">Additional comments</string> + <string name="report_remote_instance">Forward to %1$s</string> + <string name="failed_report">Failed to report</string> + <string name="failed_fetch_posts">Failed to fetch posts</string> + <string name="report_description_1">The report will be sent to your server moderator. You can provide an explanation of why you are reporting this account below:</string> + <string name="report_description_remote_instance">The account is from another server. Send an anonymized copy of the report there as well?</string> + <string name="title_accounts">Accounts</string> + <string name="failed_search">Failed to search</string> + + <string name="pref_title_enable_swipe_for_tabs">Enable swipe gesture to switch between tabs</string> + <string name="pref_title_show_stat_inline">Show post statistics in timeline</string> + <string name="pref_title_show_notifications_filter">Show Notifications filter</string> + + <string name="create_poll_title">Poll</string> + <string name="label_duration">Duration</string> + <string name="duration_indefinite">Indefinite</string> + <string name="duration_5_min">5 minutes</string> + <string name="duration_30_min">30 minutes</string> + <string name="duration_1_hour">1 hour</string> + <string name="duration_6_hours">6 hours</string> + <string name="duration_1_day">1 day</string> + <string name="duration_3_days">3 days</string> + <string name="duration_7_days">7 days</string> + <string name="duration_14_days">14 days</string> + <string name="duration_30_days">30 days</string> + <string name="duration_60_days">60 days</string> + <string name="duration_90_days">90 days</string> + <string name="duration_180_days">180 days</string> + <string name="duration_365_days">365 days</string> + <string name="duration_no_change">(No change)</string> + <string name="add_poll_choice">Add choice</string> + <string name="poll_allow_multiple_choices">Multiple choices</string> + <string name="poll_new_choice_hint">Choice %1$d</string> + <string name="edit_poll">Edit</string> + <string name="post_lookup_error_format">Error looking up post %1$s</string> + + <string name="no_drafts">You don\'t have any drafts.</string> + <string name="no_scheduled_posts">You don\'t have any scheduled posts.</string> + <string name="no_announcements">There are no announcements.</string> + <string name="no_lists">You don\'t have any lists.</string> + <string name="warning_scheduling_interval">Mastodon has a minimum scheduling interval of 5 minutes.</string> + <string name="pref_title_show_self_username">Show username in toolbars</string> + <string name="pref_title_show_cards_in_timelines">Show link previews in timelines</string> + <string name="pref_title_confirm_reblogs">Show confirmation before boosting</string> + <string name="pref_title_confirm_favourites">Show confirmation before favoriting</string> + <string name="pref_title_confirm_follows">Show confirmation before following</string> + <string name="pref_title_hide_top_toolbar">Hide the title of the top toolbar</string> + <string name="pref_title_wellbeing_mode">Wellbeing</string> + <string name="account_note_hint">Your private note about this account</string> + <string name="account_note_saved">Saved!</string> + + <string name="wellbeing_mode_notice">Some information that might affect your mental wellbeing will be hidden. This includes:\n\n + - Favorite/Boost/Follow notifications\n + - Favorite/Boost count on posts\n + - Follower/Post stats on profiles\n\n + Push-notifications will not be affected, but you can review your notification preferences manually. + </string> + <string name="review_notifications">Review Notifications</string> + <string name="limit_notifications">Limit timeline notifications</string> + <string name="wellbeing_hide_stats_posts">Hide quantitative stats on posts</string> + <string name="wellbeing_hide_stats_profile">Hide quantitative stats on profiles</string> + <plurals name="error_upload_max_media_reached"> + <item quantity="one">You cannot upload more than %1$d media attachment.</item> + <item quantity="other">You cannot upload more than %1$d media attachments.</item> + </plurals> + <string name="dialog_delete_list_warning">Do you really want to delete the list %1$s?</string> + <string name="drafts_post_failed_to_send">This post failed to send!</string> + + <string name="drafts_failed_loading_reply">Failed loading Reply information</string> + <string name="draft_deleted">Draft deleted</string> + <string name="drafts_post_reply_removed">The post you drafted a reply to has been removed</string> + + <string name="follow_requests_info">Even though your account is not locked, the %1$s staff thought you might want to review follow requests from these accounts manually.</string> + + <string name="action_subscribe_account">Subscribe</string> + <string name="action_unsubscribe_account">Unsubscribe</string> + + <string name="tusky_compose_post_quicksetting_label">Compose Post</string> + + <string name="account_date_joined">Joined %1$s</string> + + <string name="saving_draft">Saving draft…</string> + + <string name="tips_push_notification_migration">Re-login all accounts to enable push notification support.</string> + <string name="dialog_push_notification_migration">In order to use push notifications via UnifiedPush, Tusky needs permission to subscribe to notifications on your Mastodon server. This requires a re-login to change the OAuth scopes granted to Tusky. Using the re-login option here or in "Account preferences" will preserve all of your local drafts and cache.</string> + <string name="dialog_push_notification_migration_other_accounts">You have re-logged into your current account to grant push subscription permission to Tusky. However, you still have other accounts that have not been migrated this way. Switch to them and re-login one by one in order to enable UnifiedPush notifications support.</string> + + <string name="delete_scheduled_post_warning">Delete this scheduled post?</string> + + <string name="instance_rule_info">By logging in you agree to the rules of %1$s.</string> + <string name="instance_rule_title">%1$s rules</string> + <string name="language_display_name_format">%1$s (%2$s)</string> + + <string name="report_category_violation">Rule violation</string> + <string name="report_category_spam">Spam</string> + <string name="report_category_legal">Legal</string> + <string name="report_category_other">Other</string> + + <string name="action_unfollow_hashtag_format">Unfollow #%1$s?</string> + <string name="action_refresh">Refresh</string> + <string name="mute_notifications_switch">Mute notifications</string> + + <!-- Reading order preference --> + <string name="pref_title_reading_order">Reading order</string> + <string name="pref_reading_order_oldest_first">Oldest first</string> + <string name="pref_reading_order_newest_first">Newest first</string> + + <string name="status_edit_info">Edited: %1$s</string> + <string name="status_created_info">Created: %1$s</string> + <string name="error_missing_edits">Your server knows that this post was edited, but does not have a copy of the edits, so they can\'t be shown to you.\n\nThis is <a href="https://github.com/mastodon/mastodon/issues/25398">Mastodon issue #25398</a>.</string> + <string name="a11y_label_loading_thread">Loading thread</string> + + <!--@knossos@fosstodon.org created 2023-01-07 --> + <string name="accessibility_talking_about_tag">%1$d people are talking about hashtag %2$s</string> + <string name="total_usage">Total usage</string> + <string name="total_accounts">Total accounts</string> + + <!-- User friendly error messages for different network errors --> + <string name="socket_timeout_exception">Contacting your server took too long</string> + + <!-- Error messages, displayed in snackbars, when something failed --> + <string name="ui_error_unknown">unknown reason</string> + <string name="ui_error_bookmark">Bookmarking post failed: %1$s</string> + <string name="ui_error_clear_notifications">Clearing notifications failed: %1$s</string> + <string name="ui_error_favourite">Favoriting post failed: %1$s</string> + <string name="ui_error_reblog">Boosting post failed: %1$s</string> + <string name="ui_error_vote">Voting in poll failed: %1$s</string> + <string name="ui_error_accept_follow_request">Accepting follow request failed: %1$s</string> + <string name="ui_error_reject_follow_request">Rejecting follow request failed: %1$s</string> + <string name="ui_error_translate">Could not translate: %1$s</string> + + <!-- Success messages, displayed in snackbars, when an action succeeded --> + <string name="ui_success_accepted_follow_request">Follow request accepted</string> + <string name="ui_success_rejected_follow_request">Follow request blocked</string> + + <string name="hint_filter_title">My filter</string> + <string name="label_filter_title">Title</string> + <string name="filter_action_warn">Warn</string> + <string name="filter_action_hide">Hide</string> + <string name="filter_description_warn">Hide with a warning</string> + <string name="filter_description_hide">Hide completely</string> + <string name="label_filter_action">Filter action</string> + <string name="label_filter_context">Filter contexts</string> + <string name="label_filter_keywords">Keywords or phrases to filter</string> + <string name="action_add">Add</string> + <string name="filter_keyword_display_format">%1$s (whole word)</string> + <string name="filter_keyword_addition_title">Add keyword</string> + <string name="filter_edit_keyword_title">Edit keyword</string> + <string name="filter_description_format">%1$s: %2$s</string> + + <string name="help_empty_home">This is your <b>home timeline</b>. It shows the recent posts of the accounts + you follow.\n\nTo explore accounts you can either discover them in one of the other timelines. + For example the local timeline of your instance [iconics gmd_group]. Or you can search them + by name [iconics gmd_search]; for example search for Tusky to find our Mastodon account.</string> + <string name="help_empty_conversations">Here are your <b>private messages</b>; sometimes called conversations or direct messages (DM). + \n\nPrivate messages are created by setting the visibility [iconics gmd_public] of a post to [iconics gmd_mail] <i>Direct</i> and + mentioning one or more users in the text. + \n\nFor example you can start on the profile view of an account and tap the create button [iconics gmd_edit] and change the visibility. + </string> + <string name="help_empty_lists">This is your <b>lists view</b>. You can define a number of private lists and add accounts to that. + \n\n + NOTE that you can only add accounts you follow to your lists. + \n\n + These lists can be used as a tab in Account preferences [iconics gmd_account_circle] [iconics gmd_navigate_next] Tabs. + </string> + + <string name="load_newest_notifications">Load newest notifications</string> + <string name="about_copy">Copy version and device information</string> + <string name="about_copied">Copied version and device information</string> + <string name="error_media_playback">Playback failed: %1$s</string> + <string name="dialog_delete_filter_text">Delete filter \'%1$s\'?"</string> + <string name="dialog_delete_filter_positive_action">Delete</string> + <string name="dialog_save_profile_changes_message">Do you want to save your profile changes?</string> + + <string name="list_reply_policy_none">No one</string> + <string name="list_reply_policy_list">Members of the list</string> + <string name="list_reply_policy_followed">Any followed user</string> + <string name="list_reply_policy_label">Show replies to</string> + + <string name="unknown_notification_type">Unknown notification type</string> +</resources> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..b027918 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,182 @@ +<resources> + + <style name="TextSizeSmallest"> + <item name="status_text_small">10sp</item> + <item name="status_text_medium">12sp</item> + <item name="status_text_large">14sp</item> + </style> + + <style name="TextSizeSmall"> + <item name="status_text_small">12sp</item> + <item name="status_text_medium">14sp</item> + <item name="status_text_large">16sp</item> + </style> + + <style name="TextSizeMedium"> + <item name="status_text_small">14sp</item> + <item name="status_text_medium">16sp</item> + <item name="status_text_large">18sp</item> + </style> + + <style name="TextSizeLarge"> + <item name="status_text_small">16sp</item> + <item name="status_text_medium">18sp</item> + <item name="status_text_large">20sp</item> + </style> + + <style name="TextSizeLargest"> + <item name="status_text_small">18sp</item> + <item name="status_text_medium">20sp</item> + <item name="status_text_large">22sp</item> + </style> + + <style name="SplashTheme" parent="Theme.SplashScreen"> + <item name="windowSplashScreenAnimatedIcon">@drawable/ic_splash</item> + <item name="windowSplashScreenBackground">@color/tusky_grey_20</item> + </style> + + <style name="TuskyTheme" parent="TuskyBaseTheme" /> + + <style name="TuskyDialogActivityTheme" parent="@style/TuskyTheme" /> + + <style name="TuskyBaseTheme" parent="Theme.MaterialComponents.DayNight.NoActionBar"> + <!-- Provide default text sizes. These are overwritten in BaseActivity, but + if they are missing then the Android Studio layout preview crashes + with java.lang.reflect.InvocationTargetException --> + <item name="status_text_small">14sp</item> + <item name="status_text_medium">16sp</item> + <item name="status_text_large">18sp</item> + + <item name="colorPrimary">@color/colorPrimary</item> + <item name="colorOnPrimary">@color/colorOnPrimary</item> + + <item name="colorSecondary">@color/colorSecondary</item> + <item name="colorOnSecondary">@color/white</item> + + <item name="colorSurface">@color/colorSurface</item> + + <item name="colorPrimaryDark">@color/colorPrimaryDark</item> + + <item name="android:colorBackground">@color/colorBackground</item> + <item name="colorBackgroundAccent">@color/colorBackgroundAccent</item> + <item name="colorBackgroundHighlight">@color/colorBackgroundHighlight</item> + <item name="windowBackgroundColor">@color/windowBackground</item> + + <item name="android:textColorPrimary">@color/textColorPrimary</item> + <item name="android:textColorSecondary">@color/textColorSecondary</item> + <item name="android:textColorTertiary">@color/textColorTertiary</item> + + <item name="iconColor">@color/iconColor</item> + + <item name="android:listDivider">@drawable/status_divider</item> + <item name="dividerColor">@color/dividerColor</item> + + <item name="textColorDisabled">@color/textColorDisabled</item> + + <item name="materialDrawerStyle">@style/TuskyDrawerStyle</item> + <item name="materialDrawerHeaderStyle">@style/TuskyDrawerHeaderStyle</item> + + <item name="alertDialogTheme">@style/TuskyDialog</item> + <item name="snackbarButtonStyle">@style/TuskyButton.TextButton</item> + <item name="appBarLayoutStyle">@style/Widget.MaterialComponents.AppBarLayout.Surface</item> + + <item name="minTouchTargetSize">32dp</item> <!-- this affects RadioButton size --> + <item name="elevationOverlayEnabled">false</item> <!-- disable the automatic tinting of surfaces with elevation in dark mode --> + + <item name="swipeRefreshLayoutProgressSpinnerBackgroundColor">?attr/colorSurface</item> + + <item name="chipStyle">@style/Widget.MaterialComponents.Chip.Choice</item> + + <item name="preferenceTheme">@style/TuskyPreferenceTheme</item> + </style> + + <style name="TuskyBlackThemeBase" parent="TuskyBaseTheme"> + <item name="colorPrimaryDark">@color/tusky_grey_05</item> + + <item name="colorOnPrimary">@color/black</item> + + <item name="android:colorBackground">@color/black</item> + <item name="windowBackgroundColor">@color/black</item> + + <item name="colorSurface">@color/tusky_grey_10</item> + + <item name="iconColor">@color/tusky_grey_40</item> + <item name="colorBackgroundHighlight">@color/tusky_grey_40</item> + <item name="colorBackgroundAccent">@color/tusky_grey_20</item> + + <item name="dividerColor">@color/tusky_grey_20</item> + </style> + + <style name="TuskyBlackTheme" parent="TuskyBlackThemeBase" /> + + <style name="ViewMediaActivity.AppBarLayout" parent="ThemeOverlay.AppCompat"> + <item name="android:colorControlNormal">@color/white</item> + </style> + + <style name="TuskyDialog" parent="@style/ThemeOverlay.MaterialComponents.Dialog.Alert"> + <item name="android:letterSpacing">0</item> + <item name="dialogCornerRadius">8dp</item> + <item name="android:colorBackground">@color/colorBackground</item> + </style> + + <style name="TuskyDialogFragmentStyle" parent="@style/ThemeOverlay.MaterialComponents.Dialog"> + <item name="dialogCornerRadius">8dp</item> + </style> + + <style name="TuskyTabAppearance" parent="Widget.MaterialComponents.TabLayout"> + <item name="tabTextAppearance">?android:attr/textAppearanceButton</item> + <item name="android:textAllCaps">true</item> + <item name="tabIndicatorHeight">3dp</item> + </style> + + <style name="TuskyPreferenceTheme" parent="@style/PreferenceThemeOverlay.v14.Material"> + <item name="android:tint">?iconColor</item> + </style> + + <style name="TuskyImageButton" parent="@style/Widget.MaterialComponents.Button.UnelevatedButton"> + <item name="android:tint">?android:attr/textColorTertiary</item> + <item name="android:background">?attr/selectableItemBackgroundBorderless</item> + </style> + + <style name="TuskyButton" parent="Widget.MaterialComponents.Button"> + <item name="android:letterSpacing">0</item> + </style> + + <style name="TuskyButton.Outlined" parent="Widget.MaterialComponents.Button.OutlinedButton"> + <item name="strokeColor">?attr/colorBackgroundAccent</item> + <item name="android:letterSpacing">0</item> + </style> + + <style name="TuskyButton.TextButton" parent="Widget.MaterialComponents.Button.TextButton"> + <item name="android:letterSpacing">0</item> + </style> + + <style name="TuskyTextInput" parent="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.Dense"> + <item name="boxStrokeColor">@color/text_input_layout_box_stroke_color</item> + <item name="android:textColorHint">?android:attr/textColorTertiary</item> + </style> + + <style name="TuskyDrawerStyle" parent ="Widget.MaterialDrawerStyle"> + <item name="materialDrawerBackground">?android:colorBackground</item> + <item name="materialDrawerPrimaryIcon">?iconColor</item> + <item name="materialDrawerSecondaryIcon">?iconColor</item> + <item name="materialDrawerDividerColor">?dividerColor</item> + </style> + + <style name="TuskyDrawerHeaderStyle" parent ="Widget.MaterialDrawerHeaderStyle"> + <item name="materialDrawerHeaderSelectionText">?android:textColorPrimary</item> + <item name="materialDrawerHeaderSelectionSubtext">?android:textColorPrimary</item> + </style> + + <!-- customize the shape of the avatars in account selection list --> + <style name="BezelImageView"> + <item name="materialDrawerMaskDrawable">@drawable/materialdrawer_shape_small</item> + <item name="materialDrawerDrawCircularShadow">false</item> + </style> + + <!-- Used in exo_player_control_view.xml. Unmodified values are 5dp each --> + <style name="TuskyExoPlayerPlayPause" parent="ExoStyledControls.Button.Center.PlayPause"> + <item name="android:layout_marginLeft">20dp</item> + <item name="android:layout_marginRight">20dp</item> + </style> +</resources> diff --git a/app/src/main/res/values/theme_colors.xml b/app/src/main/res/values/theme_colors.xml new file mode 100644 index 0000000..02d335d --- /dev/null +++ b/app/src/main/res/values/theme_colors.xml @@ -0,0 +1,41 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <color name="colorPrimary">@color/tusky_blue_dark</color> + <color name="colorSecondary">@color/tusky_blue_dark</color> + <color name="colorSurface">@color/tusky_grey_95</color> + <color name="colorPrimaryDark">@color/tusky_grey_70</color> + + <color name="colorOnPrimary">@color/white</color> + + <color name="colorBackground">@color/white</color> + <color name="windowBackground">@color/tusky_grey_80</color> + + <color name="textColorPrimary">@color/tusky_grey_10</color> + <color name="textColorSecondary">@color/tusky_grey_20</color> + <color name="textColorTertiary">@color/tusky_grey_30</color> + <color name="textColorDisabled">@color/tusky_grey_70</color> + + <color name="iconColor">@color/tusky_grey_50</color> + + <color name="colorBackgroundAccent">@color/tusky_grey_70</color> + <color name="colorBackgroundHighlight">@color/tusky_grey_50</color> + <color name="dividerColor">@color/tusky_grey_70</color> + <color name="dividerColorOther">@color/tusky_grey_90</color> + + <color name="favoriteButtonActiveColor">@color/tusky_orange_light</color> + + <color name="warning_color">@color/tusky_red</color> + + <color name="headerBackgroundFilter">@color/header_background_filter_light</color> + + <bool name="lightNavigationBar">true</bool> + + <color name="botBadgeForeground">@color/tusky_grey_20</color> + <color name="botBadgeBackground">@color/white</color> + + <color name="toolbar_icon_background">#CCEBEFF4</color> + + <!-- colors used to show inserted/deleted text --> + <color name="view_edits_background_insert">@color/tusky_green_lighter</color> + <color name="view_edits_background_delete">@color/tusky_red_lighter</color> +</resources> diff --git a/app/src/main/res/values/toot_button.xml b/app/src/main/res/values/toot_button.xml new file mode 100644 index 0000000..14ba616 --- /dev/null +++ b/app/src/main/res/values/toot_button.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <bool name="show_small_toot_button">true</bool> + <dimen name="toot_button_width">48dp</dimen> + <dimen name="toot_button_horizontal_padding">12dp</dimen> + +</resources> \ No newline at end of file diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml new file mode 100644 index 0000000..61f9cde --- /dev/null +++ b/app/src/main/res/xml/file_paths.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<paths> + <external-path name="my_images" path="." /> + <cache-path name="*" path="." /> +</paths> \ No newline at end of file diff --git a/app/src/main/res/xml/locales_config.xml b/app/src/main/res/xml/locales_config.xml new file mode 100644 index 0000000..a409a91 --- /dev/null +++ b/app/src/main/res/xml/locales_config.xml @@ -0,0 +1,52 @@ +<?xml version="1.0" encoding="utf-8"?> +<locale-config xmlns:android="http://schemas.android.com/apk/res/android"> + <locale android:name="en"/> + <locale android:name="ca"/> + <locale android:name="cs"/> + <locale android:name="cy"/> + <locale android:name="de"/> + <locale android:name="en-GB"/> + <locale android:name="en"/> + <locale android:name="eo"/> + <locale android:name="es"/> + <locale android:name="eu"/> + <locale android:name="fr"/> + <locale android:name="ga"/> + <locale android:name="gd"/> + <locale android:name="gl"/> + <locale android:name="is"/> + <locale android:name="it"/> + <locale android:name="hu"/> + <locale android:name="nl"/> + <locale android:name="nb-NO"/> + <locale android:name="oc"/> + <locale android:name="or"/> + <locale android:name="pl"/> + <locale android:name="pt-BR"/> + <locale android:name="pt-PT"/> + <locale android:name="sl"/> + <locale android:name="sv"/> + <locale android:name="kab"/> + <locale android:name="vi"/> + <locale android:name="tr"/> + <locale android:name="be"/> + <locale android:name="bg"/> + <locale android:name="ru"/> + <locale android:name="uk"/> + <locale android:name="ar"/> + <locale android:name="ckb"/> + <locale android:name="bn-BD"/> + <locale android:name="bn-IN"/> + <locale android:name="fa"/> + <locale android:name="hi"/> + <locale android:name="sa"/> + <locale android:name="ta"/> + <locale android:name="th"/> + <locale android:name="ko"/> + <locale android:name="zh-TW"/> + <locale android:name="zh-SG"/> + <locale android:name="zh-MO"/> + <locale android:name="zh-CN"/> + <locale android:name="zh-HK"/> + <locale android:name="ja"/> +</locale-config> diff --git a/app/src/main/res/xml/searchable.xml b/app/src/main/res/xml/searchable.xml new file mode 100644 index 0000000..c3fbef7 --- /dev/null +++ b/app/src/main/res/xml/searchable.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<searchable xmlns:android="http://schemas.android.com/apk/res/android" + android:label="@string/app_name" + android:hint="@string/hint_search"> +</searchable> \ No newline at end of file diff --git a/app/src/main/res/xml/share_shortcuts.xml b/app/src/main/res/xml/share_shortcuts.xml new file mode 100644 index 0000000..54ecd5d --- /dev/null +++ b/app/src/main/res/xml/share_shortcuts.xml @@ -0,0 +1,8 @@ +<shortcuts xmlns:android="http://schemas.android.com/apk/res/android"> + + <share-target android:targetClass="com.keylesspalace.tusky.MainActivity"> + <data android:mimeType="text/plain" /> + <category android:name="com.keylesspalace.tusky.Share" /> + </share-target> + +</shortcuts> \ No newline at end of file diff --git a/app/src/test/java/android/text/SpannableString.kt b/app/src/test/java/android/text/SpannableString.kt new file mode 100644 index 0000000..dc8cd83 --- /dev/null +++ b/app/src/test/java/android/text/SpannableString.kt @@ -0,0 +1,49 @@ +package android.text + +// Used for stubbing Android implementation without slow & buggy Robolectric things +@Suppress("unused") +class SpannableString(private val text: CharSequence) : Spannable { + + override fun setSpan(what: Any?, start: Int, end: Int, flags: Int) { + throw NotImplementedError() + } + + override fun <T : Any?> getSpans(start: Int, end: Int, type: Class<T>?): Array<T> { + throw NotImplementedError() + } + + override fun removeSpan(what: Any?) { + throw NotImplementedError() + } + + override fun toString(): String { + return "FakeSpannableString[text=$text]" + } + + override val length: Int + get() = text.length + + override fun nextSpanTransition(start: Int, limit: Int, type: Class<*>?): Int { + throw NotImplementedError() + } + + override fun getSpanEnd(tag: Any?): Int { + throw NotImplementedError() + } + + override fun getSpanFlags(tag: Any?): Int { + throw NotImplementedError() + } + + override fun get(index: Int): Char { + throw NotImplementedError() + } + + override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { + throw NotImplementedError() + } + + override fun getSpanStart(tag: Any?): Int { + throw NotImplementedError() + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt new file mode 100644 index 0000000..395a310 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/BottomSheetActivityTest.kt @@ -0,0 +1,319 @@ +/* Copyright 2018 Levi Bard + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky + +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import at.connyduck.calladapter.networkresult.NetworkResult +import com.keylesspalace.tusky.entity.SearchResult +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.network.MastodonApi +import java.util.Date +import kotlin.time.Duration.Companion.milliseconds +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.resetMain +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mockito.eq +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock + +@OptIn(ExperimentalCoroutinesApi::class) +class BottomSheetActivityTest { + + @get:Rule + val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule() + + private lateinit var activity: FakeBottomSheetActivity + private lateinit var apiMock: MastodonApi + private val accountQuery = "http://mastodon.foo.bar/@User" + private val statusQuery = "http://mastodon.foo.bar/@User/345678" + private val nonexistentStatusQuery = "http://mastodon.foo.bar/@User/345678000" + private val nonMastodonQuery = "http://medium.com/@correspondent/345678" + private val emptyResult = NetworkResult.success(SearchResult(emptyList(), emptyList(), emptyList())) + + private val account = TimelineAccount( + id = "1", + localUsername = "admin", + username = "admin", + displayName = "Ad Min", + note = "This is their bio", + url = "http://mastodon.foo.bar/@User", + avatar = "" + ) + private val accountResult = NetworkResult.success(SearchResult(listOf(account), emptyList(), emptyList())) + + private val status = Status( + id = "1", + url = statusQuery, + account = account, + inReplyToId = null, + inReplyToAccountId = null, + reblog = null, + content = "omgwat", + createdAt = Date(), + editedAt = null, + emojis = emptyList(), + reblogsCount = 0, + favouritesCount = 0, + repliesCount = 0, + reblogged = false, + favourited = false, + bookmarked = false, + sensitive = false, + spoilerText = "", + visibility = Status.Visibility.PUBLIC, + attachments = ArrayList(), + mentions = emptyList(), + tags = emptyList(), + application = null, + pinned = false, + muted = false, + poll = null, + card = null, + language = null, + filtered = emptyList() + ) + private val statusResult = NetworkResult.success(SearchResult(emptyList(), listOf(status), emptyList())) + + @Before + fun setup() { + apiMock = mock { + onBlocking { search(eq(accountQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn accountResult + onBlocking { search(eq(statusQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn statusResult + onBlocking { search(eq(nonexistentStatusQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn accountResult + onBlocking { search(eq(nonMastodonQuery), eq(null), anyBoolean(), eq(null), eq(null), eq(null)) } doReturn emptyResult + } + + activity = FakeBottomSheetActivity(apiMock) + } + + @Test + fun beginEndSearch_setIsSearching_isSearchingAfterBegin() { + activity.onBeginSearch("https://mastodon.foo.bar/@User") + assertTrue(activity.isSearching()) + } + + @Test + fun beginEndSearch_setIsSearching_isNotSearchingAfterEnd() { + val validUrl = "https://mastodon.foo.bar/@User" + activity.onBeginSearch(validUrl) + activity.onEndSearch(validUrl) + assertFalse(activity.isSearching()) + } + + @Test + fun beginEndSearch_setIsSearching_doesNotCancelSearchWhenResponseFromPreviousSearchIsReceived() { + val validUrl = "https://mastodon.foo.bar/@User" + val invalidUrl = "" + + activity.onBeginSearch(validUrl) + activity.onEndSearch(invalidUrl) + assertTrue(activity.isSearching()) + } + + @Test + fun cancelActiveSearch() { + val url = "https://mastodon.foo.bar/@User" + + activity.onBeginSearch(url) + activity.cancelActiveSearch() + assertFalse(activity.isSearching()) + } + + @Test + fun getCancelSearchRequested_detectsURL() { + val firstUrl = "https://mastodon.foo.bar/@User" + val secondUrl = "https://mastodon.foo.bar/@meh" + + activity.onBeginSearch(firstUrl) + activity.cancelActiveSearch() + + activity.onBeginSearch(secondUrl) + assertTrue(activity.getCancelSearchRequested(firstUrl)) + assertFalse(activity.getCancelSearchRequested(secondUrl)) + } + + @Test + fun search_inIdealConditions_returnsRequestedResults_forAccount() = runTest { + Dispatchers.setMain(StandardTestDispatcher(testScheduler)) + try { + activity.viewUrl(accountQuery) + testScheduler.advanceTimeBy(100.milliseconds) + assertEquals(account.id, activity.accountId) + } finally { + Dispatchers.resetMain() + } + } + + @Test + fun search_inIdealConditions_returnsRequestedResults_forStatus() = runTest { + Dispatchers.setMain(StandardTestDispatcher(testScheduler)) + try { + activity.viewUrl(statusQuery) + testScheduler.advanceTimeBy(100.milliseconds) + assertEquals(status.id, activity.statusId) + } finally { + Dispatchers.resetMain() + } + } + + @Test + fun search_inIdealConditions_returnsRequestedResults_forNonMastodonURL() = runTest { + Dispatchers.setMain(StandardTestDispatcher(testScheduler)) + try { + activity.viewUrl(nonMastodonQuery) + testScheduler.advanceTimeBy(100.milliseconds) + assertEquals(nonMastodonQuery, activity.link) + } finally { + Dispatchers.resetMain() + } + } + + @Test + fun search_withNoResults_appliesRequestedFallbackBehavior() = runTest { + Dispatchers.setMain(StandardTestDispatcher(testScheduler)) + try { + for (fallbackBehavior in listOf( + PostLookupFallbackBehavior.OPEN_IN_BROWSER, + PostLookupFallbackBehavior.DISPLAY_ERROR + )) { + activity.viewUrl(nonMastodonQuery, fallbackBehavior) + testScheduler.advanceTimeBy(100.milliseconds) + assertEquals(nonMastodonQuery, activity.link) + assertEquals(fallbackBehavior, activity.fallbackBehavior) + } + } finally { + Dispatchers.resetMain() + } + } + + @Test + fun search_doesNotRespectUnrelatedResult() = runTest { + Dispatchers.setMain(StandardTestDispatcher(testScheduler)) + try { + activity.viewUrl(nonexistentStatusQuery) + testScheduler.advanceTimeBy(100.milliseconds) + assertEquals(nonexistentStatusQuery, activity.link) + assertEquals(null, activity.accountId) + } finally { + Dispatchers.resetMain() + } + } + + @Test + fun search_withCancellation_doesNotLoadUrl_forAccount() = runTest { + Dispatchers.setMain(StandardTestDispatcher(testScheduler)) + try { + activity.viewUrl(accountQuery) + assertTrue(activity.isSearching()) + activity.cancelActiveSearch() + assertFalse(activity.isSearching()) + assertEquals(null, activity.accountId) + } finally { + Dispatchers.resetMain() + } + } + + @Test + fun search_withCancellation_doesNotLoadUrl_forStatus() = runTest { + Dispatchers.setMain(StandardTestDispatcher(testScheduler)) + try { + activity.viewUrl(accountQuery) + activity.cancelActiveSearch() + assertEquals(null, activity.accountId) + } finally { + Dispatchers.resetMain() + } + } + + @Test + fun search_withCancellation_doesNotLoadUrl_forNonMastodonURL() = runTest { + Dispatchers.setMain(StandardTestDispatcher(testScheduler)) + try { + activity.viewUrl(nonMastodonQuery) + activity.cancelActiveSearch() + assertEquals(null, activity.searchUrl) + } finally { + Dispatchers.resetMain() + } + } + + @Test + fun search_withPreviousCancellation_completes() = runTest { + Dispatchers.setMain(StandardTestDispatcher(testScheduler)) + try { + // begin/cancel account search + activity.viewUrl(accountQuery) + activity.cancelActiveSearch() + + // begin status search + activity.viewUrl(statusQuery) + + // ensure that search is still ongoing + assertTrue(activity.isSearching()) + + // return searchResults + testScheduler.advanceTimeBy(100.milliseconds) + + // ensure that the result of the status search was recorded + // and the account search wasn't + assertEquals(status.id, activity.statusId) + assertEquals(null, activity.accountId) + } finally { + Dispatchers.resetMain() + } + } + + class FakeBottomSheetActivity(api: MastodonApi) : BottomSheetActivity() { + + var statusId: String? = null + var accountId: String? = null + var link: String? = null + var fallbackBehavior: PostLookupFallbackBehavior? = null + + init { + mastodonApi = api + bottomSheet = mock() + } + + override fun openLink(url: String) { + this.link = url + } + + override fun viewAccount(id: String) { + this.accountId = id + } + + override fun viewThread(statusId: String, url: String?) { + this.statusId = statusId + } + + override fun performUrlFallbackAction(url: String, fallbackBehavior: PostLookupFallbackBehavior) { + this.link = url + this.fallbackBehavior = fallbackBehavior + } + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/FilterV1Test.kt b/app/src/test/java/com/keylesspalace/tusky/FilterV1Test.kt new file mode 100644 index 0000000..e6ceab6 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/FilterV1Test.kt @@ -0,0 +1,348 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.keylesspalace.tusky.components.filters.EditFilterActivity +import com.keylesspalace.tusky.entity.Attachment +import com.keylesspalace.tusky.entity.Filter +import com.keylesspalace.tusky.entity.FilterV1 +import com.keylesspalace.tusky.entity.Poll +import com.keylesspalace.tusky.entity.PollOption +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.FilterModel +import java.time.Instant +import java.util.Date +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.robolectric.annotation.Config + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class FilterV1Test { + + private lateinit var filterModel: FilterModel + + @Before + fun setup() { + filterModel = FilterModel() + val filters = listOf( + FilterV1( + id = "123", + phrase = "badWord", + context = listOf(FilterV1.HOME), + expiresAt = null, + irreversible = false, + wholeWord = false + ), + FilterV1( + id = "123", + phrase = "badWholeWord", + context = listOf(FilterV1.HOME, FilterV1.PUBLIC), + expiresAt = null, + irreversible = false, + wholeWord = true + ), + FilterV1( + id = "123", + phrase = "@twitter.com", + context = listOf(FilterV1.HOME), + expiresAt = null, + irreversible = false, + wholeWord = true + ), + FilterV1( + id = "123", + phrase = "#hashtag", + context = listOf(FilterV1.HOME), + expiresAt = null, + irreversible = false, + wholeWord = true + ), + FilterV1( + id = "123", + phrase = "expired", + context = listOf(FilterV1.HOME), + expiresAt = Date.from(Instant.now().minusSeconds(10)), + irreversible = false, + wholeWord = true + ), + FilterV1( + id = "123", + phrase = "unexpired", + context = listOf(FilterV1.HOME), + expiresAt = Date.from(Instant.now().plusSeconds(3600)), + irreversible = false, + wholeWord = true + ), + FilterV1( + id = "123", + phrase = "href", + context = listOf(FilterV1.HOME), + expiresAt = null, + irreversible = false, + wholeWord = false + ) + ) + + filterModel.initWithFilters(filters) + } + + @Test + fun shouldNotFilter() { + assertEquals( + Filter.Action.NONE, + filterModel.shouldFilterStatus( + mockStatus(content = "should not be filtered") + ) + ) + } + + @Test + fun shouldFilter_whenContentMatchesBadWord() { + assertEquals( + Filter.Action.HIDE, + filterModel.shouldFilterStatus( + mockStatus(content = "one two badWord three") + ) + ) + } + + @Test + fun shouldFilter_whenContentMatchesBadWordPart() { + assertEquals( + Filter.Action.HIDE, + filterModel.shouldFilterStatus( + mockStatus(content = "one two badWordPart three") + ) + ) + } + + @Test + fun shouldFilter_whenContentMatchesBadWholeWord() { + assertEquals( + Filter.Action.HIDE, + filterModel.shouldFilterStatus( + mockStatus(content = "one two badWholeWord three") + ) + ) + } + + @Test + fun shouldNotFilter_whenContentDoesNotMatchWholeWord() { + assertEquals( + Filter.Action.NONE, + filterModel.shouldFilterStatus( + mockStatus(content = "one two badWholeWordTest three") + ) + ) + } + + @Test + fun shouldFilter_whenSpoilerTextDoesMatch() { + assertEquals( + Filter.Action.HIDE, + filterModel.shouldFilterStatus( + mockStatus( + content = "should not be filtered", + spoilerText = "badWord should be filtered" + ) + ) + ) + } + + @Test + fun shouldFilter_whenPollTextDoesMatch() { + assertEquals( + Filter.Action.HIDE, + filterModel.shouldFilterStatus( + mockStatus( + content = "should not be filtered", + spoilerText = "should not be filtered", + pollOptions = listOf("should not be filtered", "badWord") + ) + ) + ) + } + + @Test + fun shouldFilter_whenMediaDescriptionDoesMatch() { + assertEquals( + Filter.Action.HIDE, + filterModel.shouldFilterStatus( + mockStatus( + content = "should not be filtered", + spoilerText = "should not be filtered", + attachmentsDescriptions = listOf("should not be filtered", "badWord") + ) + ) + ) + } + + @Test + fun shouldFilterPartialWord_whenWholeWordFilterContainsNonAlphanumericCharacters() { + assertEquals( + Filter.Action.HIDE, + filterModel.shouldFilterStatus( + mockStatus(content = "one two someone@twitter.com three") + ) + ) + } + + @Test + fun shouldFilterHashtags() { + assertEquals( + Filter.Action.HIDE, + filterModel.shouldFilterStatus( + mockStatus(content = "#hashtag one two three") + ) + ) + } + + @Test + fun shouldFilterHashtags_whenContentIsMarkedUp() { + assertEquals( + Filter.Action.HIDE, + filterModel.shouldFilterStatus( + mockStatus(content = "<p><a href=\"https://foo.bar/tags/hashtag\" class=\"mention hashtag\" rel=\"nofollow noopener noreferrer\" target=\"_blank\">#<span>hashtag</span></a>one two three</p>") + ) + ) + } + + @Test + fun shouldNotFilterHtmlAttributes() { + assertEquals( + Filter.Action.NONE, + filterModel.shouldFilterStatus( + mockStatus(content = "<p><a href=\"https://foo.bar/\">https://foo.bar/</a> one two three</p>") + ) + ) + } + + @Test + fun shouldNotFilter_whenFilterIsExpired() { + assertEquals( + Filter.Action.NONE, + filterModel.shouldFilterStatus( + mockStatus(content = "content matching expired filter should not be filtered") + ) + ) + } + + @Test + fun shouldFilter_whenFilterIsUnexpired() { + assertEquals( + Filter.Action.HIDE, + filterModel.shouldFilterStatus( + mockStatus(content = "content matching unexpired filter should be filtered") + ) + ) + } + + @Test + fun unchangedExpiration_shouldBeNegative_whenFilterIsExpired() { + val expiredBySeconds = 3600 + val expiredDate = Date.from(Instant.now().minusSeconds(expiredBySeconds.toLong())) + val updatedDuration = EditFilterActivity.getSecondsForDurationIndex(-1, null, expiredDate) + assert(updatedDuration != null && updatedDuration <= -expiredBySeconds) + } + + @Test + fun unchangedExpiration_shouldBePositive_whenFilterIsUnexpired() { + val expiresInSeconds = 3600 + val expiredDate = Date.from(Instant.now().plusSeconds(expiresInSeconds.toLong())) + val updatedDuration = EditFilterActivity.getSecondsForDurationIndex(-1, null, expiredDate) + assert(updatedDuration != null && updatedDuration > (expiresInSeconds - 60)) + } + + companion object { + fun mockStatus( + content: String = "", + spoilerText: String = "", + pollOptions: List<String>? = null, + attachmentsDescriptions: List<String>? = null + ): Status { + return Status( + id = "123", + url = "https://mastodon.social/@Tusky/100571663297225812", + account = mock(), + inReplyToId = null, + inReplyToAccountId = null, + reblog = null, + content = content, + createdAt = Date(), + editedAt = null, + emojis = emptyList(), + reblogsCount = 0, + favouritesCount = 0, + repliesCount = 0, + reblogged = false, + favourited = false, + bookmarked = false, + sensitive = false, + spoilerText = spoilerText, + visibility = Status.Visibility.PUBLIC, + attachments = if (attachmentsDescriptions != null) { + ArrayList( + attachmentsDescriptions.map { + Attachment( + id = "1234", + url = "", + previewUrl = null, + meta = null, + type = Attachment.Type.IMAGE, + description = it, + blurhash = null + ) + } + ) + } else { + arrayListOf() + }, + mentions = listOf(), + tags = listOf(), + application = null, + pinned = false, + muted = false, + poll = if (pollOptions != null) { + Poll( + id = "1234", + expiresAt = null, + expired = false, + multiple = false, + votesCount = 0, + votersCount = 0, + options = pollOptions.map { + PollOption(it, 0) + }, + voted = false, + ownVotes = emptyList() + ) + } else { + null + }, + card = null, + language = null, + filtered = emptyList() + ) + } + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/FocalPointUtilTest.kt b/app/src/test/java/com/keylesspalace/tusky/FocalPointUtilTest.kt new file mode 100644 index 0000000..89bcf7a --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/FocalPointUtilTest.kt @@ -0,0 +1,195 @@ +/* Copyright 2018 Jochem Raat <jchmrt@riseup.net> + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky + +import com.keylesspalace.tusky.util.FocalPointUtil +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class FocalPointUtilTest { + private val eps = 0.01f + + // focal[X|Y]ToCoordinate tests + @Test + fun positiveFocalXToCoordinateTest() { + assertEquals(FocalPointUtil.focalXToCoordinate(0.4f), 0.7f, eps) + } + + @Test + fun negativeFocalXToCoordinateTest() { + assertEquals(FocalPointUtil.focalXToCoordinate(-0.8f), 0.1f, eps) + } + + @Test + fun positiveFocalYToCoordinateTest() { + assertEquals(FocalPointUtil.focalYToCoordinate(-0.2f), 0.6f, eps) + } + + @Test + fun negativeFocalYToCoordinateTest() { + assertEquals(FocalPointUtil.focalYToCoordinate(0.0f), 0.5f, eps) + } + + // isVerticalCrop tests + @Test + fun isVerticalCropTest() { + assertTrue( + FocalPointUtil.isVerticalCrop( + 2f, + 1f, + 1f, + 2f + ) + ) + } + + @Test + fun isHorizontalCropTest() { + assertFalse( + FocalPointUtil.isVerticalCrop( + 1f, + 2f, + 2f, + 1f + ) + ) + } + + @Test + fun isPerfectFitTest() { // Doesn't matter what it returns, just check it doesn't crash + FocalPointUtil.isVerticalCrop( + 3f, + 1f, + 6f, + 2f + ) + } + + // calculateScaling tests + @Test + fun perfectFitScaleDownTest() { + assertEquals( + FocalPointUtil.calculateScaling( + 2f, + 5f, + 5f, + 12.5f + ), + 0.4f, + eps + ) + } + + @Test + fun perfectFitScaleUpTest() { + assertEquals( + FocalPointUtil.calculateScaling( + 2f, + 5f, + 1f, + 2.5f + ), + 2f, + eps + ) + } + + @Test + fun verticalCropScaleUpTest() { + assertEquals( + FocalPointUtil.calculateScaling( + 2f, + 1f, + 1f, + 2f + ), + 2f, + eps + ) + } + + @Test + fun verticalCropScaleDownTest() { + assertEquals( + FocalPointUtil.calculateScaling( + 4f, + 3f, + 8f, + 24f + ), + 0.5f, + eps + ) + } + + @Test + fun horizontalCropScaleUpTest() { + assertEquals( + FocalPointUtil.calculateScaling( + 1f, + 2f, + 2f, + 1f + ), + 2f, + eps + ) + } + + @Test + fun horizontalCropScaleDownTest() { + assertEquals( + FocalPointUtil.calculateScaling( + 3f, + 4f, + 24f, + 8f + ), + 0.5f, + eps + ) + } + + // focalOffset tests + @Test + fun toLowFocalOffsetTest() { + assertEquals( + FocalPointUtil.focalOffset(2f, 8f, 1f, 0.05f), + 0f, + eps + ) + } + + @Test + fun toHighFocalOffsetTest() { + assertEquals( + FocalPointUtil.focalOffset(2f, 4f, 2f, 0.95f), + -6f, + eps + ) + } + + @Test + fun possibleFocalOffsetTest() { + assertEquals( + FocalPointUtil.focalOffset(2f, 4f, 2f, 0.7f), + -4.6f, + eps + ) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt new file mode 100644 index 0000000..35ccb3c --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/MainActivityTest.kt @@ -0,0 +1,153 @@ +package com.keylesspalace.tusky + +import android.app.Activity +import android.app.NotificationManager +import android.content.ComponentName +import android.content.Intent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import androidx.viewpager2.widget.ViewPager2 +import androidx.work.testing.WorkManagerTestInitHelper +import at.connyduck.calladapter.networkresult.NetworkResult +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.components.accountlist.AccountListActivity +import com.keylesspalace.tusky.components.systemnotifications.NotificationHelper +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.entity.Account +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.util.getSerializableExtraCompat +import java.util.Date +import kotlinx.coroutines.test.TestScope +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.robolectric.Robolectric +import org.robolectric.Shadows.shadowOf +import org.robolectric.android.util.concurrent.BackgroundExecutor.runInBackground +import org.robolectric.annotation.Config + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class MainActivityTest { + + private val context = InstrumentationRegistry.getInstrumentation().targetContext + private val account = Account( + id = "1", + localUsername = "", + username = "", + displayName = "", + createdAt = Date(), + note = "", + url = "", + avatar = "", + header = "" + ) + private val accountEntity = AccountEntity( + id = 1, + domain = "test.domain", + accessToken = "fakeToken", + clientId = "fakeId", + clientSecret = "fakeSecret", + isActive = true + ) + + @Before + fun setup() { + WorkManagerTestInitHelper.initializeTestWorkManager(context) + } + + @After + fun teardown() { + WorkManagerTestInitHelper.closeWorkDatabase() + } + + @Test + fun `clicking notification of type FOLLOW shows notification tab`() { + val intent = showNotification(Notification.Type.FOLLOW) + + val activity = startMainActivity(intent) + val currentTab = activity.findViewById<ViewPager2>(R.id.viewPager).currentItem + + val notificationTab = defaultTabs().indexOfFirst { it.id == NOTIFICATIONS } + + assertEquals(currentTab, notificationTab) + } + + @Test + fun `clicking notification of type FOLLOW_REQUEST shows follow requests`() { + val intent = showNotification(Notification.Type.FOLLOW_REQUEST) + + val activity = startMainActivity(intent) + val nextActivity = shadowOf(activity).peekNextStartedActivity() + + assertNotNull(nextActivity) + assertEquals(ComponentName(context, AccountListActivity::class.java.name), nextActivity.component) + assertEquals(AccountListActivity.Type.FOLLOW_REQUESTS, nextActivity.getSerializableExtraCompat("type")) + } + + private fun showNotification(type: Notification.Type): Intent { + val notificationManager = context.getSystemService(NotificationManager::class.java) + val shadowNotificationManager = shadowOf(notificationManager) + + NotificationHelper.createNotificationChannelsForAccount(accountEntity, context) + + runInBackground { + val notification = NotificationHelper.make( + context, + notificationManager, + Notification( + type = type, + id = "id", + account = TimelineAccount( + id = "1", + localUsername = "connyduck", + username = "connyduck@mastodon.example", + displayName = "Conny Duck", + note = "This is their bio", + url = "https://mastodon.example/@ConnyDuck", + avatar = "https://mastodon.example/system/accounts/avatars/000/150/486/original/ab27d7ddd18a10ea.jpg" + ), + status = null, + report = null + ), + accountEntity, + true + ) + notificationManager.notify("id", 1, notification) + } + + val notification = shadowNotificationManager.allNotifications.first() + return shadowOf(notification.contentIntent).savedIntent + } + + private fun startMainActivity(intent: Intent): Activity { + val controller = Robolectric.buildActivity(MainActivity::class.java, intent) + val activity = controller.get() + activity.eventHub = EventHub() + activity.accountManager = mock { + on { activeAccount } doReturn accountEntity + } + activity.draftsAlert = mock {} + activity.shareShortcutHelper = mock {} + activity.externalScope = TestScope() + activity.mastodonApi = mock { + onBlocking { accountVerifyCredentials() } doReturn NetworkResult.success(account) + onBlocking { listAnnouncements(false) } doReturn NetworkResult.success(emptyList()) + } + activity.preferences = mock(defaultAnswer = { + when (it.method.returnType) { + String::class.java -> "test" + Boolean::class.java -> false + else -> null + } + }) + controller.create().start() + return activity + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/SpanUtilsTest.kt b/app/src/test/java/com/keylesspalace/tusky/SpanUtilsTest.kt new file mode 100644 index 0000000..6d12f76 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/SpanUtilsTest.kt @@ -0,0 +1,187 @@ +package com.keylesspalace.tusky + +import android.text.Spannable +import com.keylesspalace.tusky.util.FoundMatchType +import com.keylesspalace.tusky.util.MENTION_PATTERN_STRING +import com.keylesspalace.tusky.util.PatternFinder +import com.keylesspalace.tusky.util.TAG_PATTERN_STRING +import com.keylesspalace.tusky.util.highlightSpans +import com.keylesspalace.tusky.util.twittertext.Regex +import java.util.regex.Pattern +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +/** The [Pattern.UNICODE_CHARACTER_CLASS] flag is not supported on Android, on Android it is just always on. + * Since thesse tests run on a regular Jvm, we need a to set this flag or they would behave differently. + * */ +private val urlPattern = Regex.VALID_URL_PATTERN_STRING.toPattern(Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CHARACTER_CLASS) +private val tagPattern = TAG_PATTERN_STRING.toPattern(Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CHARACTER_CLASS) +private val mentionPattern = MENTION_PATTERN_STRING.toPattern(Pattern.CASE_INSENSITIVE or Pattern.UNICODE_CHARACTER_CLASS) + +val finders = listOf( + PatternFinder("http", FoundMatchType.HTTPS_URL, urlPattern), + PatternFinder("#", FoundMatchType.TAG, tagPattern), + PatternFinder("@", FoundMatchType.MENTION, mentionPattern) +) + +@RunWith(Parameterized::class) +class SpanUtilsTest( + private val stringToHighlight: String, + private val highlights: List<Pair<Int, Int>> +) { + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun data() = listOf( + arrayOf("@mention", listOf(0 to 8)), + arrayOf("@mention@server.com", listOf(0 to 19)), + arrayOf("#tag", listOf(0 to 4)), + arrayOf("#tåg", listOf(0 to 4)), + arrayOf("https://thr.ee/meh?foo=bar&wat=@at#hmm", listOf(0 to 38)), + arrayOf("http://thr.ee/meh?foo=bar&wat=@at#hmm", listOf(0 to 37)), + arrayOf( + "one #one two: @two three : https://thr.ee/meh?foo=bar&wat=@at#hmm four #four five @five 6 #six", + listOf(4 to 8, 14 to 18, 27 to 65, 71 to 76, 82 to 87, 90 to 94) + ), + arrayOf("http://first.link https://second.link", listOf(0 to 17, 18 to 37)), + arrayOf("#test", listOf(0 to 5)), + arrayOf(" #AfterSpace", listOf(1 to 12)), + arrayOf("#BeforeSpace ", listOf(0 to 12)), + arrayOf("@#after_at", listOf(1 to 10)), + arrayOf("あいうえお#after_hiragana", listOf<Pair<Int, Int>>()), + arrayOf("##DoubleHash", listOf(1 to 12)), + arrayOf("###TripleHash", listOf(2 to 13)), + arrayOf("something#notAHashtag", listOf<Pair<Int, Int>>()), + arrayOf("test##maybeAHashtag", listOf(5 to 19)), + arrayOf("testhttp://not.a.url.com", listOf<Pair<Int, Int>>()), + arrayOf("test@notAMention", listOf<Pair<Int, Int>>()), + arrayOf("test@notAMention#notAHashtag", listOf<Pair<Int, Int>>()), + arrayOf("test@notAMention@server.com", listOf<Pair<Int, Int>>()), + // Mastodon will not highlight this mention, although it would be valid according to their regex + // arrayOf("@test@notAMention@server.com", listOf<Pair<Int, Int>>()), + arrayOf("testhttps://not.a.url.com", listOf<Pair<Int, Int>>()), + arrayOf("#hashtag1", listOf(0 to 9)), + arrayOf("#1hashtag", listOf(0 to 9)), + arrayOf("#サイクリング", listOf(0 to 7)), + arrayOf("#自転車に乗る", listOf(0 to 7)), + arrayOf("(#test)", listOf(1 to 6)), + arrayOf(")#test(", listOf<Pair<Int, Int>>()), + arrayOf("{#test}", listOf(1 to 6)), + arrayOf("[#test]", listOf(1 to 6)), + arrayOf("}#test{", listOf(1 to 6)), + arrayOf("]#test[", listOf(1 to 6)), + arrayOf("<#test>", listOf(1 to 6)), + arrayOf(">#test<", listOf(1 to 6)), + arrayOf("((#Test))", listOf(2 to 7)), + arrayOf("((##Te)st)", listOf(3 to 6)), + arrayOf("[@ConnyDuck]", listOf(1 to 11)), + arrayOf("(@ConnyDuck)", listOf(1 to 11)), + arrayOf("(@ConnyDuck@chaos.social)", listOf(1 to 24)), + arrayOf("Test(https://test.xyz/blubb(test)))))))))))", listOf(5 to 33)), + arrayOf("Test https://test.xyz/blubb(test)))))))))))", listOf(5 to 33)), + arrayOf("Test https://test.xyz/blubbtest)))))))))))", listOf(5 to 31)), + arrayOf("#https://test.com", listOf(0 to 6)), + arrayOf("#https://t", listOf(0 to 6)), + arrayOf("(https://blubb.com", listOf(1 to 18)), + arrayOf("https://example.com/path#anchor", listOf(0 to 31)), + arrayOf("test httpx2345://wrong.protocol.com", listOf<Pair<Int, Int>>()), + arrayOf("test https://nonexistent.topleveldomain.testtest", listOf<Pair<Int, Int>>()), + arrayOf("test https://example.com:1234 domain with port", listOf(5 to 29)), + arrayOf("http://1.1.1.1", listOf<Pair<Int, Int>>()), + arrayOf("http://foo.bar/?q=Test%20URL-encoded%20stuff", listOf(0 to 44)), + arrayOf("http://userid:password@example.com", listOf<Pair<Int, Int>>()), + arrayOf("http://userid@example.com", listOf<Pair<Int, Int>>()), + arrayOf("http://foo.com/blah_blah_(brackets)_(again)", listOf(0 to 43)), + arrayOf("test example.com/no/protocol", listOf<Pair<Int, Int>>()), + arrayOf("protocol only https://", listOf<Pair<Int, Int>>()), + arrayOf("no tld https://test", listOf<Pair<Int, Int>>()), + arrayOf("mention in url https://test.com/@test@domain.cat", listOf(15 to 48)), + arrayOf("#hash_tag", listOf(0 to 9)), + arrayOf("#hashtag_", listOf(0 to 9)), + arrayOf("#hashtag_#tag", listOf(0 to 9, 9 to 13)), + arrayOf("#hash_tag#tag", listOf(0 to 9)), + arrayOf("_#hashtag", listOf(1 to 9)), + arrayOf("@@ConnyDuck@chaos.social", listOf(1 to 24)), + arrayOf("http://https://connyduck.at", listOf(7 to 27)), + arrayOf("https://https://connyduck.at", listOf(8 to 28)), + arrayOf("http:// http://connyduck.at", listOf(8 to 27)), + arrayOf("https:// https://connyduck.at", listOf(9 to 29)), + arrayOf("https:// #test https://connyduck.at", listOf(9 to 14, 15 to 35)), + arrayOf("http:// @connyduck http://connyduck.at", listOf(8 to 18, 19 to 38)), + // emojis count as multiple characters + arrayOf("😜https://connyduck.at", listOf(2 to 22)), + arrayOf("😜#tag", listOf(2 to 6)), + arrayOf("😜@user@mastodon.example", listOf(2 to 24)), + ) + } + + @Test + fun testHighlighting() { + val inputSpannable = FakeSpannable(stringToHighlight) + inputSpannable.highlightSpans(0xffffff, finders) + + assertEquals(highlights.size, inputSpannable.spans.size) + + inputSpannable.spans + .sortedBy { span -> span.start } + .forEachIndexed { index, span -> + assertEquals(highlights[index].first, span.start) + assertEquals(highlights[index].second, span.end) + } + } +} + +class FakeSpannable(private val text: String) : Spannable { + val spans = mutableListOf<BoundedSpan>() + + override fun setSpan(what: Any?, start: Int, end: Int, flags: Int) { + spans.add(BoundedSpan(what, start, end)) + } + + @Suppress("UNCHECKED_CAST") + override fun <T : Any> getSpans(start: Int, end: Int, type: Class<T>): Array<T> { + return spans.filter { it.start >= start && it.end <= end && type.isInstance(it.span) } + .map { it.span } + .toTypedArray() as Array<T> + } + + override fun removeSpan(what: Any?) { + spans.removeIf { span -> span.span == what } + } + + override fun toString(): String { + return text + } + + override val length: Int + get() = text.length + + class BoundedSpan(val span: Any?, val start: Int, val end: Int) + + override fun nextSpanTransition(start: Int, limit: Int, type: Class<*>?): Int { + throw NotImplementedError() + } + + override fun getSpanEnd(tag: Any?): Int { + throw NotImplementedError() + } + + override fun getSpanFlags(tag: Any?): Int { + throw NotImplementedError() + } + + override fun get(index: Int): Char { + return text[index] + } + + override fun subSequence(startIndex: Int, endIndex: Int): CharSequence { + return text.subSequence(startIndex, endIndex) + } + + override fun getSpanStart(tag: Any?): Int { + throw NotImplementedError() + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt b/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt new file mode 100644 index 0000000..6fd1c6a --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/StatusComparisonTest.kt @@ -0,0 +1,208 @@ +package com.keylesspalace.tusky + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.keylesspalace.tusky.di.NetworkModule +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.viewdata.StatusViewData +import com.squareup.moshi.adapter +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class StatusComparisonTest { + + @Test + fun `two equal statuses - should be equal`() { + assertEquals(createStatus(), createStatus()) + } + + @Test + fun `status with different id - should not be equal`() { + assertNotEquals(createStatus(), createStatus(id = "987654321")) + } + + @Test + fun `status with different content - should not be equal`() { + val content: String = """ + \u003cp\u003e\u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.social/@ConnyDuck\" class=\"u-url mention\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"\u003e@\u003cspan\u003eConnyDuck@mastodon.social\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e 123\u003c/p\u003e + """.trimIndent() + assertNotEquals(createStatus(), createStatus(content = content)) + } + + @Test + fun `accounts with different notes in json - should not be equal`() { + assertNotEquals(createStatus(note = "Test"), createStatus(note = "Test 123456")) + } + + private val moshi = NetworkModule.providesMoshi() + + @Test + fun `two equal status view data - should be equal`() { + val viewdata1 = StatusViewData.Concrete( + status = createStatus(), + isExpanded = false, + isShowingContent = false, + isCollapsed = false + ) + val viewdata2 = StatusViewData.Concrete( + status = createStatus(), + isExpanded = false, + isShowingContent = false, + isCollapsed = false + ) + assertEquals(viewdata1, viewdata2) + } + + @Test + fun `status view data with different isExpanded - should not be equal`() { + val viewdata1 = StatusViewData.Concrete( + status = createStatus(), + isExpanded = true, + isShowingContent = false, + isCollapsed = false + ) + val viewdata2 = StatusViewData.Concrete( + status = createStatus(), + isExpanded = false, + isShowingContent = false, + isCollapsed = false + ) + assertNotEquals(viewdata1, viewdata2) + } + + @Test + fun `status view data with different statuses- should not be equal`() { + val viewdata1 = StatusViewData.Concrete( + status = createStatus(content = "whatever"), + isExpanded = true, + isShowingContent = false, + isCollapsed = false + ) + val viewdata2 = StatusViewData.Concrete( + status = createStatus(), + isExpanded = false, + isShowingContent = false, + isCollapsed = false + ) + assertNotEquals(viewdata1, viewdata2) + } + + @OptIn(ExperimentalStdlibApi::class) + private fun createStatus( + id: String = "123456", + content: String = """ + \u003cp\u003e\u003cspan class=\"h-card\"\u003e\u003ca href=\"https://mastodon.social/@ConnyDuck\" class=\"u-url mention\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"\u003e@\u003cspan\u003eConnyDuck@mastodon.social\u003c/span\u003e\u003c/a\u003e\u003c/span\u003e Hi\u003c/p\u003e + """.trimIndent(), + note: String = "" + ): Status { + val statusJson = """ + { + "id": "$id", + "created_at": "2022-02-26T09:54:45.000Z", + "in_reply_to_id": null, + "in_reply_to_account_id": null, + "sensitive": false, + "spoiler_text": "", + "visibility": "public", + "language": null, + "uri": "https://pixelfed.social/p/connyduck/403124983655733325", + "url": "https://pixelfed.social/p/connyduck/403124983655733325", + "replies_count": 3, + "reblogs_count": 28, + "favourites_count": 6, + "edited_at": null, + "favourited": true, + "reblogged": false, + "muted": false, + "bookmarked": false, + "content": "$content", + "reblog": null, + "account": { + "id": "419352", + "username": "connyduck", + "acct": "connyduck@pixelfed.social", + "display_name": "Conny Duck", + "locked": false, + "bot": false, + "discoverable": false, + "group": false, + "created_at": "2018-08-14T00:00:00.000Z", + "note": "$note", + "url": "https://pixelfed.social/connyduck", + "avatar": "https://files.mastodon.social/cache/accounts/avatars/000/419/352/original/31ce660c53962e0c.jpeg", + "avatar_static": "https://files.mastodon.social/cache/accounts/avatars/000/419/352/original/31ce660c53962e0c.jpeg", + "header": "https://mastodon.social/headers/original/missing.png", + "header_static": "https://mastodon.social/headers/original/missing.png", + "followers_count": 2, + "following_count": 0, + "statuses_count": 70, + "last_status_at": "2022-03-07", + "emojis": [], + "fields": [] + }, + "media_attachments": [ + { + "id": "107863694400783337", + "type": "image", + "url": "https://files.mastodon.social/cache/media_attachments/files/107/863/694/400/783/337/original/71c5bad1756bbc8f.jpg", + "preview_url": "https://files.mastodon.social/cache/media_attachments/files/107/863/694/400/783/337/small/71c5bad1756bbc8f.jpg", + "remote_url": "https://pixelfed-prod.nyc3.cdn.digitaloceanspaces.com/public/m/_v2/1138/affc38a2b-1c5f41/JRKoMNoj6dKa/9mXs0Fetvj4KwRbKypt8C1PZNVd7d3dQqod4roLZ.jpg", + "preview_remote_url": null, + "text_url": null, + "meta": { + "original": { + "width": 1371, + "height": 1080, + "size": "1371x1080", + "aspect": 1.2694444444444444 + }, + "small": { + "width": 451, + "height": 355, + "size": "451x355", + "aspect": 1.2704225352112677 + } + }, + "description": "Oilpainting of a kingfisher, photographed on my easel", + "blurhash": "UUG91|?wxHV@WTkDs.V?xZa_I:WBNFR*WBRk" + }, + { + "id": "107863694727565058", + "type": "image", + "url": "https://files.mastodon.social/cache/media_attachments/files/107/863/694/727/565/058/original/68daef05be7ac6b6.jpg", + "preview_url": "https://files.mastodon.social/cache/media_attachments/files/107/863/694/727/565/058/small/68daef05be7ac6b6.jpg", + "remote_url": "https://pixelfed-prod.nyc3.cdn.digitaloceanspaces.com/public/m/_v2/1138/affc38a2b-1c5f41/nBVJUnrEIjfO/M6i8GSP44Iv230KWXnMpvVobOqASXY3EkImyxySS.jpg", + "preview_remote_url": null, + "text_url": null, + "meta": { + "original": { + "width": 1087, + "height": 1080, + "size": "1087x1080", + "aspect": 1.0064814814814815 + }, + "small": { + "width": 401, + "height": 398, + "size": "401x398", + "aspect": 1.0075376884422111 + } + }, + "description": "Oilpainting of a kingfisher", + "blurhash": "U89u4pPJ4:SoJ6NNnkoxoBtSx0Von-RiNgt8" + } + ], + "mentions": [], + "tags": [], + "emojis": [], + "card": null, + "poll": null + } + """.trimIndent() + return moshi.adapter<Status>().fromJson(statusJson)!! + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/StringUtilsTest.kt b/app/src/test/java/com/keylesspalace/tusky/StringUtilsTest.kt new file mode 100644 index 0000000..518af95 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/StringUtilsTest.kt @@ -0,0 +1,38 @@ +package com.keylesspalace.tusky + +import com.keylesspalace.tusky.util.isLessThan +import com.keylesspalace.tusky.util.isLessThanOrEqual +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test + +class StringUtilsTest { + @Test + fun isLessThan() { + val lessList = listOf( + "abc" to "bcd", + "ab" to "abc", + "cb" to "abc", + "1" to "2" + ) + lessList.forEach { (l, r) -> assertTrue("$l < $r", l.isLessThan(r)) } + val notLessList = lessList.map { (l, r) -> r to l } + listOf( + "abc" to "abc" + ) + notLessList.forEach { (l, r) -> assertFalse("not $l < $r", l.isLessThan(r)) } + } + + @Test + fun isLessThanOrEqual() { + val lessList = listOf( + "abc" to "bcd", + "ab" to "abc", + "cb" to "abc", + "1" to "2", + "abc" to "abc" + ) + lessList.forEach { (l, r) -> assertTrue("$l < $r", l.isLessThanOrEqual(r)) } + val notLessList = lessList.filterNot { (l, r) -> l == r }.map { (l, r) -> r to l } + notLessList.forEach { (l, r) -> assertFalse("not $l < $r", l.isLessThanOrEqual(r)) } + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt b/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt new file mode 100644 index 0000000..cd07c21 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/TuskyApplication.kt @@ -0,0 +1,35 @@ +/* Copyright 2020 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky + +import android.app.Application +import android.content.SharedPreferences +import com.keylesspalace.tusky.di.PreferencesEntryPoint +import dagger.hilt.internal.GeneratedComponent +import de.c1710.filemojicompat_defaults.DefaultEmojiPackList +import de.c1710.filemojicompat_ui.helpers.EmojiPackHelper +import org.mockito.kotlin.mock + +// override TuskyApplication for Robolectric tests, only initialize the necessary stuff +class TuskyApplication : Application(), PreferencesEntryPoint, GeneratedComponent { + + override fun onCreate() { + super.onCreate() + EmojiPackHelper.init(this, DefaultEmojiPackList.get(this)) + } + + override fun preferences(): SharedPreferences = mock {} +} diff --git a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivityTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivityTest.kt new file mode 100644 index 0000000..c1a4c8a --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeActivityTest.kt @@ -0,0 +1,716 @@ +/* + * Copyright 2018 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.components.compose + +import android.content.Intent +import android.os.Looper.getMainLooper +import android.widget.EditText +import androidx.lifecycle.viewmodel.initializer +import androidx.lifecycle.viewmodel.viewModelFactory +import androidx.test.ext.junit.runners.AndroidJUnit4 +import at.connyduck.calladapter.networkresult.NetworkResult +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.components.instanceinfo.InstanceInfoRepository +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.dao.InstanceDao +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.db.entity.EmojisEntity +import com.keylesspalace.tusky.db.entity.InstanceInfoEntity +import com.keylesspalace.tusky.di.NetworkModule +import com.keylesspalace.tusky.entity.Instance +import com.keylesspalace.tusky.entity.InstanceConfiguration +import com.keylesspalace.tusky.entity.InstanceV1 +import com.keylesspalace.tusky.entity.SearchResult +import com.keylesspalace.tusky.entity.StatusConfiguration +import com.keylesspalace.tusky.finders +import com.keylesspalace.tusky.network.MastodonApi +import com.squareup.moshi.adapter +import java.util.Locale +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.SupervisorJob +import okhttp3.ResponseBody +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.robolectric.Robolectric +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config +import org.robolectric.fakes.RoboMenuItem +import retrofit2.HttpException +import retrofit2.Response + +/** + * Created by charlag on 3/7/18. + */ + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class ComposeActivityTest { + private lateinit var activity: ComposeActivity + private lateinit var accountManagerMock: AccountManager + private lateinit var apiMock: MastodonApi + + private val instanceDomain = "example.domain" + + private val account = AccountEntity( + id = 1, + domain = instanceDomain, + accessToken = "token", + clientId = "id", + clientSecret = "secret", + isActive = true, + accountId = "1", + username = "username", + displayName = "Display Name", + profilePictureUrl = "", + notificationsEnabled = true, + notificationsMentioned = true, + notificationsFollowed = true, + notificationsFollowRequested = false, + notificationsReblogged = true, + notificationsFavorited = true, + notificationSound = true, + notificationVibration = true, + notificationLight = true + ) + private var instanceV1ResponseCallback: (() -> InstanceV1)? = null + private var instanceResponseCallback: (() -> Instance)? = null + private var composeOptions: ComposeActivity.ComposeOptions? = null + private val moshi = NetworkModule.providesMoshi() + + @Before + fun setupActivity() { + val controller = Robolectric.buildActivity(ComposeActivity::class.java) + activity = controller.get() + + accountManagerMock = mock { + on { activeAccount } doReturn account + } + + apiMock = mock { + onBlocking { getCustomEmojis() } doReturn NetworkResult.success(emptyList()) + onBlocking { getInstance() } doReturn instanceResponseCallback?.invoke().let { instance -> + if (instance == null) { + NetworkResult.failure(HttpException(Response.error<ResponseBody>(404, "Not found".toResponseBody()))) + } else { + NetworkResult.success(instance) + } + } + onBlocking { getInstanceV1() } doReturn instanceV1ResponseCallback?.invoke().let { instance -> + if (instance == null) { + NetworkResult.failure(Throwable()) + } else { + NetworkResult.success(instance) + } + } + onBlocking { search(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn NetworkResult.success( + SearchResult(emptyList(), emptyList(), emptyList()) + ) + } + + val instanceDaoMock: InstanceDao = mock { + onBlocking { getInstanceInfo(any()) } doReturn + InstanceInfoEntity(instanceDomain, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null) + onBlocking { getEmojiInfo(any()) } doReturn + EmojisEntity(instanceDomain, emptyList()) + } + + val dbMock: AppDatabase = mock { + on { instanceDao() } doReturn instanceDaoMock + } + + val instanceInfoRepo = InstanceInfoRepository(apiMock, dbMock, accountManagerMock, CoroutineScope(SupervisorJob())) + + val viewModel = ComposeViewModel( + apiMock, + accountManagerMock, + mock(), + mock(), + mock(), + instanceInfoRepo + ) + activity.intent = Intent(activity, ComposeActivity::class.java).apply { + putExtra(ComposeActivity.COMPOSE_OPTIONS_EXTRA, composeOptions) + } + + val testViewModelFactory = viewModelFactory { + initializer { viewModel } + } + + activity.accountManager = accountManagerMock + activity.viewModelProviderFactory = testViewModelFactory + + activity.preferences = mock(defaultAnswer = { + when (it.method.returnType) { + String::class.java -> "test" + Boolean::class.java -> false + else -> null + } + }) + + activity.highlightFinders = finders + + controller.create().start() + shadowOf(getMainLooper()).idle() + } + + @Test + fun whenCloseButtonPressedAndEmpty_finish() { + clickUp() + assertTrue(activity.isFinishing) + } + + @Test + fun whenCloseButtonPressedNotEmpty_notFinish() { + insertSomeTextInContent() + clickUp() + assertFalse(activity.isFinishing) + // We would like to check for dialog but Robolectric doesn't work with AppCompat v7 yet + } + + @Test + fun whenModifiedInitialState_andCloseButtonPressed_notFinish() { + composeOptions = ComposeActivity.ComposeOptions(modifiedInitialState = true) + setupActivity() + clickUp() + assertFalse(activity.isFinishing) + } + + @Test + fun whenBackButtonPressedAndEmpty_finish() { + clickBack() + assertTrue(activity.isFinishing) + } + + @Test + fun whenBackButtonPressedNotEmpty_notFinish() { + insertSomeTextInContent() + clickBack() + assertFalse(activity.isFinishing) + // We would like to check for dialog but Robolectric doesn't work with AppCompat v7 yet + } + + @Test + fun whenModifiedInitialState_andBackButtonPressed_notFinish() { + composeOptions = ComposeActivity.ComposeOptions(modifiedInitialState = true) + setupActivity() + clickBack() + assertFalse(activity.isFinishing) + } + + @Test + fun whenMaximumTootCharsIsNull_defaultLimitIsUsed() { + instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(null) } + setupActivity() + assertEquals(InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT, activity.maximumTootCharacters) + } + + @Test + fun whenMaximumTootCharsIsPopulated_customLimitIsUsed() { + val customMaximum = 1000 + instanceResponseCallback = { getInstanceWithCustomConfiguration(customMaximum) } + setupActivity() + shadowOf(getMainLooper()).idle() + assertEquals(customMaximum, activity.maximumTootCharacters) + } + + @Test + fun whenOnlyLegacyMaximumTootCharsIsPopulated_customLimitIsUsed() { + val customMaximum = 1000 + instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(customMaximum) } + setupActivity() + shadowOf(getMainLooper()).idle() + assertEquals(customMaximum, activity.maximumTootCharacters) + } + + @Test + fun whenOnlyConfigurationMaximumTootCharsIsPopulated_customLimitIsUsed() { + val customMaximum = 1000 + instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(null, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum)) } + setupActivity() + shadowOf(getMainLooper()).idle() + assertEquals(customMaximum, activity.maximumTootCharacters) + } + + @Test + fun whenDifferentCharLimitsArePopulated_statusConfigurationLimitIsUsed() { + val customMaximum = 1000 + instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(customMaximum, getCustomInstanceConfiguration(maximumStatusCharacters = customMaximum * 2)) } + setupActivity() + shadowOf(getMainLooper()).idle() + assertEquals(customMaximum * 2, activity.maximumTootCharacters) + } + + @Test + fun whenTextContainsNoUrl_everyCharacterIsCounted() { + val content = "This is test content please ignore thx " + insertSomeTextInContent(content) + assertEquals(content.length, activity.calculateTextLength()) + } + + @Test + fun whenTextContainsEmoji_emojisAreCountedAsOneCharacter() { + val content = "Test 😜" + insertSomeTextInContent(content) + assertEquals(6, activity.calculateTextLength()) + } + + @Test + fun whenTextContainsUrlWithEmoji_ellipsizedUrlIsCountedCorrectly() { + val content = "https://🤪.com" + insertSomeTextInContent(content) + assertEquals(InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL, activity.calculateTextLength()) + } + + @Test + fun whenTextContainsUrl_onlyEllipsizedURLIsCounted() { + val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM" + val additionalContent = "Check out this @image #search result: " + insertSomeTextInContent(additionalContent + url) + assertEquals(additionalContent.length + InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL, activity.calculateTextLength()) + } + + @Test + fun whenTextContainsShortUrls_allUrlsGetEllipsized() { + val shortUrl = "https://tusky.app" + val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM" + val additionalContent = " Check out this @image #search result: " + insertSomeTextInContent(shortUrl + additionalContent + url) + assertEquals(additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2), activity.calculateTextLength()) + } + + @Test + fun whenTextContainsMultipleURLs_allURLsGetEllipsized() { + val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM" + val additionalContent = " Check out this @image #search result: " + insertSomeTextInContent(url + additionalContent + url) + assertEquals(additionalContent.length + (InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL * 2), activity.calculateTextLength()) + } + + @Test + fun whenTextContainsUrl_onlyEllipsizedURLIsCounted_withCustomConfiguration() { + val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM" + val additionalContent = "Check out this @image #search result: " + val customUrlLength = 16 + instanceResponseCallback = { getInstanceWithCustomConfiguration(null, customUrlLength) } + setupActivity() + shadowOf(getMainLooper()).idle() + insertSomeTextInContent(additionalContent + url) + assertEquals(additionalContent.length + customUrlLength, activity.calculateTextLength()) + } + + @Test + fun whenTextContainsUrl_onlyEllipsizedURLIsCounted_withCustomConfigurationV1() { + val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM" + val additionalContent = "Check out this @image #search result: " + val customUrlLength = 16 + instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) } + setupActivity() + shadowOf(getMainLooper()).idle() + insertSomeTextInContent(additionalContent + url) + assertEquals(additionalContent.length + customUrlLength, activity.calculateTextLength()) + } + + @Test + fun whenTextContainsShortUrls_allUrlsGetEllipsized_withCustomConfiguration() { + val shortUrl = "https://tusky.app" + val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM" + val additionalContent = " Check out this @image #search result: " + val customUrlLength = 18 // The intention is that this is longer than shortUrl.length + instanceResponseCallback = { getInstanceWithCustomConfiguration(null, customUrlLength) } + setupActivity() + shadowOf(getMainLooper()).idle() + insertSomeTextInContent(shortUrl + additionalContent + url) + assertEquals(additionalContent.length + (customUrlLength * 2), activity.calculateTextLength()) + } + + @Test + fun whenTextContainsShortUrls_allUrlsGetEllipsized_withCustomConfigurationV1() { + val shortUrl = "https://tusky.app" + val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM" + val additionalContent = " Check out this @image #search result: " + val customUrlLength = 18 // The intention is that this is longer than shortUrl.length + instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) } + setupActivity() + shadowOf(getMainLooper()).idle() + insertSomeTextInContent(shortUrl + additionalContent + url) + assertEquals(additionalContent.length + (customUrlLength * 2), activity.calculateTextLength()) + } + + @Test + fun whenTextContainsMultipleURLs_allURLsGetEllipsized_withCustomConfiguration() { + val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM" + val additionalContent = " Check out this @image #search result: " + val customUrlLength = 16 + instanceResponseCallback = { getInstanceWithCustomConfiguration(null, customUrlLength) } + setupActivity() + shadowOf(getMainLooper()).idle() + insertSomeTextInContent(url + additionalContent + url) + assertEquals(additionalContent.length + (customUrlLength * 2), activity.calculateTextLength()) + } + + @Test + fun whenTextContainsMultipleURLs_allURLsGetEllipsized_withCustomConfigurationV1() { + val url = "https://www.google.dk/search?biw=1920&bih=990&tbm=isch&sa=1&ei=bmDrWuOoKMv6kwWOkIaoDQ&q=indiana+jones+i+hate+snakes+animated&oq=indiana+jones+i+hate+snakes+animated&gs_l=psy-ab.3...54174.55443.0.55553.9.7.0.0.0.0.255.333.1j0j1.2.0....0...1c.1.64.psy-ab..7.0.0....0.40G-kcDkC6A#imgdii=PSp15hQjN1JqvM:&imgrc=H0hyE2JW5wrpBM" + val additionalContent = " Check out this @image #search result: " + val customUrlLength = 16 + instanceV1ResponseCallback = { getInstanceV1WithCustomConfiguration(configuration = getCustomInstanceConfiguration(charactersReservedPerUrl = customUrlLength)) } + setupActivity() + shadowOf(getMainLooper()).idle() + insertSomeTextInContent(url + additionalContent + url) + assertEquals(additionalContent.length + (customUrlLength * 2), activity.calculateTextLength()) + } + + @Test + fun whenSelectionIsEmpty_specialTextIsInsertedAtCaret() { + val editor = activity.findViewById<EditText>(R.id.composeEditField) + val insertText = "#" + editor.setText("Some text") + + for (caretIndex in listOf(9, 1, 0)) { + editor.setSelection(caretIndex) + activity.prependSelectedWordsWith(insertText) + // Text should be inserted at caret + assertEquals("Unexpected value at $caretIndex", insertText, editor.text.substring(caretIndex, caretIndex + insertText.length)) + + // Caret should be placed after inserted text + assertEquals(caretIndex + insertText.length, editor.selectionStart) + assertEquals(caretIndex + insertText.length, editor.selectionEnd) + } + } + + @Test + fun whenSelectionDoesNotIncludeWordBreak_noSpecialTextIsInserted() { + val editor = activity.findViewById<EditText>(R.id.composeEditField) + val insertText = "#" + val originalText = "Some text" + val selectionStart = 1 + val selectionEnd = 4 + editor.setText(originalText) + editor.setSelection(selectionStart, selectionEnd) // "ome" + activity.prependSelectedWordsWith(insertText) + + // Text and selection should be unmodified + assertEquals(originalText, editor.text.toString()) + assertEquals(selectionStart, editor.selectionStart) + assertEquals(selectionEnd, editor.selectionEnd) + } + + @Test + fun whenSelectionIncludesWordBreaks_startsOfAllWordsArePrepended() { + val editor = activity.findViewById<EditText>(R.id.composeEditField) + val insertText = "#" + val originalText = "one two three four" + val selectionStart = 2 + val originalSelectionEnd = 15 + val modifiedSelectionEnd = 18 + editor.setText(originalText) + editor.setSelection(selectionStart, originalSelectionEnd) // "e two three f" + activity.prependSelectedWordsWith(insertText) + + // text should be inserted at word starts inside selection + assertEquals("one #two #three #four", editor.text.toString()) + + // selection should be expanded accordingly + assertEquals(selectionStart, editor.selectionStart) + assertEquals(modifiedSelectionEnd, editor.selectionEnd) + } + + @Test + fun whenSelectionIncludesEnd_textIsNotAppended() { + val editor = activity.findViewById<EditText>(R.id.composeEditField) + val insertText = "#" + val originalText = "Some text" + val selectionStart = 7 + val selectionEnd = 9 + editor.setText(originalText) + editor.setSelection(selectionStart, selectionEnd) // "xt" + activity.prependSelectedWordsWith(insertText) + + // Text and selection should be unmodified + assertEquals(originalText, editor.text.toString()) + assertEquals(selectionStart, editor.selectionStart) + assertEquals(selectionEnd, editor.selectionEnd) + } + + @Test + fun whenSelectionIncludesStartAndStartIsAWord_textIsPrepended() { + val editor = activity.findViewById<EditText>(R.id.composeEditField) + val insertText = "#" + val originalText = "Some text" + val selectionStart = 0 + val selectionEnd = 3 + editor.setText(originalText) + editor.setSelection(selectionStart, selectionEnd) // "Som" + activity.prependSelectedWordsWith(insertText) + + // Text should be inserted at beginning + assert(editor.text.startsWith(insertText)) + + // selection should be expanded accordingly + assertEquals(selectionStart, editor.selectionStart) + assertEquals(selectionEnd + insertText.length, editor.selectionEnd) + } + + @Test + fun whenSelectionIncludesStartAndStartIsNotAWord_textIsNotPrepended() { + val editor = activity.findViewById<EditText>(R.id.composeEditField) + val insertText = "#" + val originalText = " Some text" + val selectionStart = 0 + val selectionEnd = 1 + editor.setText(originalText) + editor.setSelection(selectionStart, selectionEnd) // " " + activity.prependSelectedWordsWith(insertText) + + // Text and selection should be unmodified + assertEquals(originalText, editor.text.toString()) + assertEquals(selectionStart, editor.selectionStart) + assertEquals(selectionEnd, editor.selectionEnd) + } + + @Test + fun whenSelectionBeginsAtWordStart_textIsPrepended() { + val editor = activity.findViewById<EditText>(R.id.composeEditField) + val insertText = "#" + val originalText = "Some text" + val selectionStart = 5 + val selectionEnd = 9 + editor.setText(originalText) + editor.setSelection(selectionStart, selectionEnd) // "text" + activity.prependSelectedWordsWith(insertText) + + // Text is prepended + assertEquals("Some #text", editor.text.toString()) + + // Selection is expanded accordingly + assertEquals(selectionStart, editor.selectionStart) + assertEquals(selectionEnd + insertText.length, editor.selectionEnd) + } + + @Test + fun whenSelectionEndsAtWordStart_textIsAppended() { + val editor = activity.findViewById<EditText>(R.id.composeEditField) + val insertText = "#" + val originalText = "Some text" + val selectionStart = 1 + val selectionEnd = 5 + editor.setText(originalText) + editor.setSelection(selectionStart, selectionEnd) // "ome " + activity.prependSelectedWordsWith(insertText) + + // Text is prepended + assertEquals("Some #text", editor.text.toString()) + + // Selection is expanded accordingly + assertEquals(selectionStart, editor.selectionStart) + assertEquals(selectionEnd + insertText.length, editor.selectionEnd) + } + + @Test + fun whenNoLanguageIsGiven_defaultLanguageIsSelected() { + assertEquals(Locale.getDefault().language, activity.selectedLanguage) + } + + @Test + fun languageGivenInComposeOptionsIsRespected() { + val language = "no" + composeOptions = ComposeActivity.ComposeOptions(language = language) + setupActivity() + assertEquals(language, activity.selectedLanguage) + } + + @Test + fun modernLanguageCodeIsUsed() { + // https://github.com/tuskyapp/Tusky/issues/2903 + // "ji" was deprecated in favor of "yi" + composeOptions = ComposeActivity.ComposeOptions(language = "ji") + setupActivity() + assertEquals("yi", activity.selectedLanguage) + } + + @Test + fun unknownLanguageGivenInComposeOptionsIsRespected() { + val language = "zzz" + composeOptions = ComposeActivity.ComposeOptions(language = language) + setupActivity() + assertEquals(language, activity.selectedLanguage) + } + + @Test + fun sampleFriendicaInstanceResponseIsDeserializable() { + // https://github.com/tuskyapp/Tusky/issues/4100 + instanceResponseCallback = { getSampleFriendicaInstance() } + setupActivity() + shadowOf(getMainLooper()).idle() + assertEquals(FRIENDICA_MAXIMUM, activity.maximumTootCharacters) + } + + private fun clickUp() { + val menuItem = RoboMenuItem(android.R.id.home) + activity.onOptionsItemSelected(menuItem) + } + + private fun clickBack() { + activity.onBackPressedDispatcher.onBackPressed() + } + + private fun insertSomeTextInContent(text: String? = null) { + activity.findViewById<EditText>(R.id.composeEditField).setText(text ?: "Some text") + } + + private fun getInstanceWithCustomConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): Instance { + return Instance( + domain = "https://example.token", + version = "2.6.3", + configuration = getConfiguration(maximumStatusCharacters, charactersReservedPerUrl), + pleroma = null, + rules = emptyList() + ) + } + + private fun getConfiguration(maximumStatusCharacters: Int?, charactersReservedPerUrl: Int?): Instance.Configuration { + return Instance.Configuration( + Instance.Configuration.Urls(), + Instance.Configuration.Accounts(1), + Instance.Configuration.Statuses( + maximumStatusCharacters ?: InstanceInfoRepository.DEFAULT_CHARACTER_LIMIT, + InstanceInfoRepository.DEFAULT_MAX_MEDIA_ATTACHMENTS, + charactersReservedPerUrl ?: InstanceInfoRepository.DEFAULT_CHARACTERS_RESERVED_PER_URL + ), + Instance.Configuration.MediaAttachments(0, 0, 0, 0, 0), + Instance.Configuration.Polls(0, 0, 0, 0), + Instance.Configuration.Translation(false) + ) + } + + private fun getInstanceV1WithCustomConfiguration(maximumLegacyTootCharacters: Int? = null, configuration: InstanceConfiguration? = null): InstanceV1 { + return InstanceV1( + uri = "https://example.token", + version = "2.6.3", + maxTootChars = maximumLegacyTootCharacters, + pollConfiguration = null, + configuration = configuration, + maxMediaAttachments = null, + pleroma = null, + uploadLimit = null, + rules = emptyList() + ) + } + + private fun getCustomInstanceConfiguration(maximumStatusCharacters: Int? = null, charactersReservedPerUrl: Int? = null): InstanceConfiguration { + return InstanceConfiguration( + statuses = StatusConfiguration( + maxCharacters = maximumStatusCharacters, + maxMediaAttachments = null, + charactersReservedPerUrl = charactersReservedPerUrl + ), + mediaAttachments = null, + polls = null + ) + } + + @OptIn(ExperimentalStdlibApi::class) + private fun getSampleFriendicaInstance(): Instance { + return moshi.adapter<Instance>().fromJson(sampleFriendicaResponse)!! + } + + companion object { + private const val FRIENDICA_MAXIMUM = 200000 + + // https://github.com/tuskyapp/Tusky/issues/4100 + private val sampleFriendicaResponse = """{ + "domain": "loma.ml", + "title": "[ˈloma]", + "version": "2.8.0 (compatible; Friendica 2023.09-rc)", + "source_url": "https://git.friendi.ca/friendica/friendica", + "description": "loma.ml ist eine Friendica Community im Fediverse auf der vorwiegend DE \uD83C\uDDE9\uD83C\uDDEA gesprochen wird. \\r\\nServer in Germany/EU \uD83C\uDDE9\uD83C\uDDEA \uD83C\uDDEA\uD83C\uDDFA. Open to all with fun in new. \\r\\nServer in Deutschland. Offen für alle mit Spaß an Neuen.", + "usage": { + "users": { + "active_month": 125 + } + }, + "thumbnail": { + "url": "https://loma.ml/ad/friendica-banner.jpg" + }, + "languages": [ + "de" + ], + "configuration": { + "statuses": { + "max_characters": $FRIENDICA_MAXIMUM + }, + "media_attachments": { + "supported_mime_types": { + "image/jpeg": "jpg", + "image/jpg": "jpg", + "image/png": "png", + "image/gif": "gif" + }, + "image_size_limit": 10485760 + } + }, + "registrations": { + "enabled": true, + "approval_required": false + }, + "contact": { + "email": "anony@miz.ed", + "account": { + "id": "9632", + "username": "webm", + "acct": "webm", + "display_name": "web m \uD83C\uDDEA\uD83C\uDDFA", + "locked": false, + "bot": false, + "discoverable": true, + "group": false, + "created_at": "2018-05-21T11:24:55.000Z", + "note": "\uD83C\uDDE9\uD83C\uDDEA Über diesen Account werden Änderungen oder geplante Beeinträchtigungen angekündigt. Wenn du einen Account auf Loma.ml besitzt, dann solltest du dich mit mir verbinden.\uD83C\uDDEA\uD83C\uDDFA Changes or planned impairments are announced via this account. If you have an account on Loma.ml, you should connect to me.\uD83C\uDD98 Fallbackaccount @webm@joinfriendica.de", + "url": "https://loma.ml/profile/webm", + "avatar": "https://loma.ml/photo/contact/320/373ebf56355ac895a09cb99264485383?ts=1686417730", + "avatar_static": "https://loma.ml/photo/contact/320/373ebf56355ac895a09cb99264485383?ts=1686417730&static=1", + "header": "https://loma.ml/photo/header/373ebf56355ac895a09cb99264485383?ts=1686417730", + "header_static": "https://loma.ml/photo/header/373ebf56355ac895a09cb99264485383?ts=1686417730&static=1", + "followers_count": 23, + "following_count": 25, + "statuses_count": 15, + "last_status_at": "2023-09-19T00:00:00.000Z", + "emojis": [], + "fields": [] + } + }, + "rules": [], + "friendica": { + "version": "2023.09-rc", + "codename": "Giant Rhubarb", + "db_version": 1539 + } + } + """.trimIndent() + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeTokenizerTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeTokenizerTest.kt new file mode 100644 index 0000000..3c801dc --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeTokenizerTest.kt @@ -0,0 +1,95 @@ +/* Copyright 2018 Levi Bard + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. */ + +package com.keylesspalace.tusky.components.compose + +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +class ComposeTokenizerTest( + private val text: CharSequence, + private val expectedStartIndex: Int, + private val expectedEndIndex: Int +) { + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun data(): Iterable<Any> { + return listOf( + arrayOf("@mention", 0, 8), + arrayOf("@ment10n", 0, 8), + arrayOf("@ment10n_", 0, 9), + arrayOf("@ment10n_n", 0, 10), + arrayOf("@ment10n_9", 0, 10), + arrayOf(" @mention", 1, 9), + arrayOf(" @ment10n", 1, 9), + arrayOf(" @ment10n_", 1, 10), + arrayOf(" @ment10n_ @", 11, 12), + arrayOf(" @ment10n_ @ment20n", 11, 19), + arrayOf(" @ment10n_ @ment20n_", 11, 20), + arrayOf(" @ment10n_ @ment20n_n", 11, 21), + arrayOf(" @ment10n_ @ment20n_9", 11, 21), + arrayOf(" @ment10n-", 1, 10), + arrayOf(" @ment10n- @", 11, 12), + arrayOf(" @ment10n- @ment20n", 11, 19), + arrayOf(" @ment10n- @ment20n-", 11, 20), + arrayOf(" @ment10n- @ment20n-n", 11, 21), + arrayOf(" @ment10n- @ment20n-9", 11, 21), + arrayOf("@ment10n@l0calhost", 0, 18), + arrayOf(" @ment10n@l0calhost", 1, 19), + arrayOf(" @ment10n_@l0calhost", 1, 20), + arrayOf(" @ment10n-@l0calhost", 1, 20), + arrayOf(" @ment10n_@l0calhost @ment20n@husky", 21, 35), + arrayOf(" @ment10n_@l0calhost @ment20n_@husky", 21, 36), + arrayOf(" @ment10n-@l0calhost @ment20n-@husky", 21, 36), + arrayOf(" @m@localhost", 1, 13), + arrayOf(" @m@localhost @a@localhost", 14, 26), + arrayOf("@m@", 0, 3), + arrayOf(" @m@ @a@asdf", 5, 12), + arrayOf(" @m@ @a@", 5, 8), + arrayOf(" @m@ @a@a", 5, 9), + arrayOf(" @m@a @a@m", 6, 10), + arrayOf("@m@m@", 5, 5), + arrayOf("#tusky@husky", 12, 12), + arrayOf(":tusky@husky", 12, 12), + arrayOf("mention", 7, 7), + arrayOf("ment10n", 7, 7), + arrayOf("mentio_", 7, 7), + arrayOf("#tusky", 0, 6), + arrayOf("#@tusky", 7, 7), + arrayOf("@#tusky", 7, 7), + arrayOf(" @#tusky", 8, 8), + arrayOf(":mastodon", 0, 9), + arrayOf(":@mastodon", 10, 10), + arrayOf("@:mastodon", 10, 10), + arrayOf(" @:mastodon", 11, 11), + arrayOf("#@:mastodon", 11, 11), + arrayOf(" #@:mastodon", 12, 12) + ) + } + } + + private val tokenizer = ComposeTokenizer() + + @Test + fun tokenIndices_matchExpectations() { + Assert.assertEquals(expectedStartIndex, tokenizer.findTokenStart(text, text.length)) + Assert.assertEquals(expectedEndIndex, tokenizer.findTokenEnd(text, text.length)) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeViewModelTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeViewModelTest.kt new file mode 100644 index 0000000..96f85c5 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/components/compose/ComposeViewModelTest.kt @@ -0,0 +1,65 @@ +package com.keylesspalace.tusky.components.compose + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.robolectric.annotation.Config + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class ComposeViewModelTest { + + private lateinit var api: MastodonApi + private lateinit var accountManager: AccountManager + private lateinit var eventHub: EventHub + private lateinit var viewModel: ComposeViewModel + + @Before + fun setup() { + api = mock() + accountManager = mock { + on { activeAccount } doReturn + AccountEntity( + id = 1, + domain = "test.domain", + accessToken = "fakeToken", + clientId = "fakeId", + clientSecret = "fakeSecret", + isActive = true + ) + } + eventHub = EventHub() + + viewModel = ComposeViewModel( + api = api, + accountManager = accountManager, + mediaUploader = mock(), + serviceClient = mock(), + draftHelper = mock(), + instanceInfoRepo = mock(), + ) + } + + @Test + fun `startingVisibility initially set to defaultPostPrivacy for post`() { + viewModel.setup(null) + + assertEquals(Status.Visibility.PUBLIC, viewModel.statusVisibility.value) + } + + @Test + fun `startingVisibility initially set to replyPostPrivacy for reply`() { + viewModel.setup(ComposeActivity.ComposeOptions(inReplyToId = "123")) + + assertEquals(Status.Visibility.UNLISTED, viewModel.statusVisibility.value) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/components/compose/StatusLengthTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/compose/StatusLengthTest.kt new file mode 100644 index 0000000..9b1404d --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/components/compose/StatusLengthTest.kt @@ -0,0 +1,84 @@ +/* + * Copyright 2023 Tusky Contributors + * + * This file is a part of Tusky. + * + * This program is free software; you can redistribute it and/or modify it under the terms of the + * GNU General Public License as published by the Free Software Foundation; either version 3 of the + * License, or (at your option) any later version. + * + * Tusky is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tusky; if not, + * see <http://www.gnu.org/licenses>. + */ + +package com.keylesspalace.tusky.components.compose + +import com.keylesspalace.tusky.FakeSpannable +import com.keylesspalace.tusky.finders +import com.keylesspalace.tusky.util.highlightSpans +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.ParameterizedRobolectricTestRunner +import org.robolectric.annotation.Config + +@RunWith(ParameterizedRobolectricTestRunner::class) +@Config(sdk = [33]) +class StatusLengthTest( + private val text: String, + private val expectedLength: Int +) { + companion object { + @ParameterizedRobolectricTestRunner.Parameters(name = "{0}") + @JvmStatic + fun data(): Iterable<Any> { + return listOf( + arrayOf("", 0), + arrayOf(" ", 1), + arrayOf("123", 3), + arrayOf("🫣", 1), + // "@user@server" should be treated as "@user" + arrayOf("123 @example@example.org", 12), + // URLs under 23 chars are treated as 23 chars + arrayOf("123 http://example.org", 27), + // URLs over 23 chars are treated as 23 chars + arrayOf("123 http://urlthatislongerthan23characters.example.org", 27), + // Short hashtags are treated as is + arrayOf("123 #basictag", 13), + // Long hashtags are *also* treated as is (not treated as 23, like URLs) + arrayOf("123 #atagthatislongerthan23characters", 37), + // urls can have balanced parenthesis, otherwise they are ignored https://github.com/tuskyapp/Tusky/issues/4425 + arrayOf("(https://en.wikipedia.org/wiki/Beethoven_(horse))", 25) + ) + } + } + + @Test + fun statusLength_matchesExpectations() { + val spannedText = FakeSpannable(text) + spannedText.highlightSpans(0, finders) + + assertEquals( + expectedLength, + ComposeActivity.statusLength(spannedText, null, 23) + ) + } + + @Test + fun statusLength_withCwText_matchesExpectations() { + val spannedText = FakeSpannable(text) + spannedText.highlightSpans(0, finders) + + val cwText = FakeSpannable( + "a @example@example.org #hashtagmention and http://example.org URL" + ) + assertEquals( + expectedLength + cwText.length, + ComposeActivity.statusLength(spannedText, cwText, 23) + ) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/components/notifications/NotificationFaker.kt b/app/src/test/java/com/keylesspalace/tusky/components/notifications/NotificationFaker.kt new file mode 100644 index 0000000..8049207 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/components/notifications/NotificationFaker.kt @@ -0,0 +1,135 @@ +package com.keylesspalace.tusky.components.notifications + +import androidx.paging.PagingSource +import androidx.room.withTransaction +import com.keylesspalace.tusky.components.timeline.Placeholder +import com.keylesspalace.tusky.components.timeline.fakeAccount +import com.keylesspalace.tusky.components.timeline.fakeStatus +import com.keylesspalace.tusky.components.timeline.toEntity +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.entity.NotificationDataEntity +import com.keylesspalace.tusky.db.entity.NotificationEntity +import com.keylesspalace.tusky.entity.Notification +import com.keylesspalace.tusky.entity.Report +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.TimelineAccount +import java.util.Date +import org.junit.Assert.assertEquals + +fun fakeNotification( + type: Notification.Type = Notification.Type.FAVOURITE, + id: String = "1", + account: TimelineAccount = fakeAccount(id = id), + status: Status? = fakeStatus(id = id), + report: Report? = null +) = Notification( + type = type, + id = id, + account = account, + status = status, + report = report +) + +fun fakeReport( + id: String = "1", + category: String = "spam", + statusIds: List<String>? = null, + createdAt: Date = Date(1712509983273), + targetAccount: TimelineAccount = fakeAccount() +) = Report( + id = id, + category = category, + statusIds = statusIds, + createdAt = createdAt, + targetAccount = targetAccount +) + +fun Notification.toNotificationDataEntity( + tuskyAccountId: Long, + isStatusExpanded: Boolean = false, + isStatusContentShowing: Boolean = false +) = NotificationDataEntity( + tuskyAccountId = tuskyAccountId, + type = type, + id = id, + account = account.toEntity(tuskyAccountId), + status = status?.toEntity( + tuskyAccountId = tuskyAccountId, + expanded = isStatusExpanded, + contentShowing = isStatusContentShowing, + contentCollapsed = true + ), + statusAccount = status?.account?.toEntity(tuskyAccountId), + report = report?.toEntity(tuskyAccountId), + reportTargetAccount = report?.targetAccount?.toEntity(tuskyAccountId) +) + +fun Placeholder.toNotificationDataEntity( + tuskyAccountId: Long +) = NotificationDataEntity( + tuskyAccountId = tuskyAccountId, + type = null, + id = id, + account = null, + status = null, + statusAccount = null, + report = null, + reportTargetAccount = null +) + +suspend fun AppDatabase.insert(notifications: List<Notification>, tuskyAccountId: Long = 1) = withTransaction { + notifications.forEach { notification -> + + timelineAccountDao().insert( + notification.account.toEntity(tuskyAccountId) + ) + + notification.report?.let { report -> + timelineAccountDao().insert( + report.targetAccount.toEntity( + tuskyAccountId = tuskyAccountId, + ) + ) + notificationsDao().insertReport(report.toEntity(tuskyAccountId)) + } + notification.status?.let { status -> + timelineAccountDao().insert( + status.account.toEntity( + tuskyAccountId = tuskyAccountId, + ) + ) + timelineStatusDao().insert( + status.toEntity( + tuskyAccountId = tuskyAccountId, + expanded = false, + contentShowing = false, + contentCollapsed = true + ) + ) + } + notificationsDao().insertNotification( + NotificationEntity( + tuskyAccountId = tuskyAccountId, + type = notification.type, + id = notification.id, + accountId = notification.account.id, + statusId = notification.status?.id, + reportId = notification.report?.id, + loading = false + ) + ) + } +} + +suspend fun AppDatabase.assertNotifications( + expected: List<NotificationDataEntity>, + tuskyAccountId: Long = 1 +) { + val pagingSource = notificationsDao().getNotifications(tuskyAccountId) + + val loadResult = pagingSource.load(PagingSource.LoadParams.Refresh(null, 100, false)) + + val loaded = (loadResult as PagingSource.LoadResult.Page).data + + assertEquals(expected, loaded) +} diff --git a/app/src/test/java/com/keylesspalace/tusky/components/notifications/NotificationsRemoteMediatorTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/notifications/NotificationsRemoteMediatorTest.kt new file mode 100644 index 0000000..0f2c9ab --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/components/notifications/NotificationsRemoteMediatorTest.kt @@ -0,0 +1,544 @@ +package com.keylesspalace.tusky.components.notifications + +import android.os.Looper.getMainLooper +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingConfig +import androidx.paging.PagingSource +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.keylesspalace.tusky.components.timeline.Placeholder +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.Converters +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.db.entity.NotificationDataEntity +import com.keylesspalace.tusky.di.NetworkModule +import java.io.IOException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.mock +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config +import retrofit2.HttpException +import retrofit2.Response + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class NotificationsRemoteMediatorTest { + + private val accountManager: AccountManager = mock { + on { activeAccount } doReturn AccountEntity( + id = 1, + domain = "mastodon.example", + accessToken = "token", + clientId = "id", + clientSecret = "secret", + isActive = true + ) + } + + private lateinit var db: AppDatabase + + private val moshi = NetworkModule.providesMoshi() + + @Before + @ExperimentalCoroutinesApi + fun setup() { + shadowOf(getMainLooper()).idle() + + val context = InstrumentationRegistry.getInstrumentation().targetContext + db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) + .addTypeConverter(Converters(moshi)) + .build() + } + + @After + @ExperimentalCoroutinesApi + fun tearDown() { + db.close() + } + + @Test + @ExperimentalPagingApi + fun `should return error when network call returns error code`() = runTest { + val remoteMediator = NotificationsRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody()) + }, + db = db, + excludes = emptySet() + ) + + val result = remoteMediator.load(LoadType.REFRESH, state()) + + assertTrue(result is RemoteMediator.MediatorResult.Error) + assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is HttpException) + assertEquals(500, (result.throwable as HttpException).code()) + } + + @Test + @ExperimentalPagingApi + fun `should return error when network call fails`() = runTest { + val remoteMediator = NotificationsRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { notifications(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException() + }, + db = db, + excludes = emptySet() + ) + + val result = remoteMediator.load(LoadType.REFRESH, state()) + + assertTrue(result is RemoteMediator.MediatorResult.Error) + assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is IOException) + } + + @Test + @ExperimentalPagingApi + fun `should not prepend notifications`() = runTest { + val remoteMediator = NotificationsRemoteMediator( + accountManager = accountManager, + api = mock(), + db = db, + excludes = emptySet() + ) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = listOf( + fakeNotification(id = "3").toNotificationDataEntity(1) + ), + prevKey = null, + nextKey = 1 + ) + ) + ) + + val result = remoteMediator.load(LoadType.PREPEND, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertTrue((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + } + + @Test + @ExperimentalPagingApi + fun `should refresh and insert placeholder when a whole page with no overlap to existing notifications is loaded`() = runTest { + val notificationsAlreadyInDb = listOf( + fakeNotification(id = "3"), + fakeNotification(id = "2"), + fakeNotification(id = "1") + ) + + db.insert(notificationsAlreadyInDb) + + val remoteMediator = NotificationsRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { notifications(limit = 3, excludes = emptySet()) } doReturn Response.success( + listOf( + fakeNotification(id = "8"), + fakeNotification(id = "7"), + fakeNotification(id = "5") + ) + ) + onBlocking { notifications(maxId = "3", limit = 3, excludes = emptySet()) } doReturn Response.success( + listOf( + fakeNotification(id = "3"), + fakeNotification(id = "2"), + fakeNotification(id = "1") + ) + ) + }, + db = db, + excludes = emptySet() + ) + + val state = state( + pages = listOf( + PagingSource.LoadResult.Page( + data = notificationsAlreadyInDb.map { it.toNotificationDataEntity(1) }, + prevKey = null, + nextKey = 0 + ) + ), + pageSize = 3 + ) + + val result = remoteMediator.load(LoadType.REFRESH, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + + db.assertNotifications( + listOf( + fakeNotification(id = "8").toNotificationDataEntity(1), + fakeNotification(id = "7").toNotificationDataEntity(1), + Placeholder(id = "5", loading = false).toNotificationDataEntity(1), + fakeNotification(id = "3").toNotificationDataEntity(1), + fakeNotification(id = "2").toNotificationDataEntity(1), + fakeNotification(id = "1").toNotificationDataEntity(1) + ) + ) + } + + @Test + @ExperimentalPagingApi + fun `should refresh and not insert placeholder when less than a whole page is loaded`() = runTest { + val notificationsAlreadyInDb = listOf( + fakeNotification(id = "3"), + fakeNotification(id = "2"), + fakeNotification(id = "1") + ) + + db.insert(notificationsAlreadyInDb) + + val remoteMediator = NotificationsRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { notifications(limit = 20, excludes = emptySet()) } doReturn Response.success( + listOf( + fakeNotification(id = "8"), + fakeNotification(id = "7"), + fakeNotification(id = "5") + ) + ) + onBlocking { notifications(maxId = "3", limit = 20, excludes = emptySet()) } doReturn Response.success( + listOf( + fakeNotification(id = "3"), + fakeNotification(id = "2"), + fakeNotification(id = "1") + ) + ) + }, + db = db, + excludes = emptySet() + ) + + val state = state( + pages = listOf( + PagingSource.LoadResult.Page( + data = notificationsAlreadyInDb.map { it.toNotificationDataEntity(1) }, + prevKey = null, + nextKey = 0 + ) + ) + ) + + val result = remoteMediator.load(LoadType.REFRESH, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + + db.assertNotifications( + listOf( + fakeNotification(id = "8").toNotificationDataEntity(1), + fakeNotification(id = "7").toNotificationDataEntity(1), + fakeNotification(id = "5").toNotificationDataEntity(1), + fakeNotification(id = "3").toNotificationDataEntity(1), + fakeNotification(id = "2").toNotificationDataEntity(1), + fakeNotification(id = "1").toNotificationDataEntity(1) + ) + ) + } + + @Test + @ExperimentalPagingApi + fun `should refresh and not insert placeholders when there is overlap with existing notifications`() = runTest { + val notificationsAlreadyInDb = listOf( + fakeNotification(id = "3"), + fakeNotification(id = "2"), + fakeNotification(id = "1") + ) + + db.insert(notificationsAlreadyInDb) + + val remoteMediator = NotificationsRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { notifications(limit = 3, excludes = emptySet()) } doReturn Response.success( + listOf( + fakeNotification(id = "6"), + fakeNotification(id = "4"), + fakeNotification(id = "3") + ) + ) + onBlocking { notifications(maxId = "3", limit = 3, excludes = emptySet()) } doReturn Response.success( + listOf( + fakeNotification(id = "3"), + fakeNotification(id = "2"), + fakeNotification(id = "1") + ) + ) + }, + db = db, + excludes = emptySet() + ) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = notificationsAlreadyInDb.map { it.toNotificationDataEntity(1) }, + prevKey = null, + nextKey = 0 + ) + ), + pageSize = 3 + ) + + val result = remoteMediator.load(LoadType.REFRESH, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + + db.assertNotifications( + listOf( + fakeNotification(id = "6").toNotificationDataEntity(1), + fakeNotification(id = "4").toNotificationDataEntity(1), + fakeNotification(id = "3").toNotificationDataEntity(1), + fakeNotification(id = "2").toNotificationDataEntity(1), + fakeNotification(id = "1").toNotificationDataEntity(1) + ) + ) + } + + @Test + @ExperimentalPagingApi + fun `should not try to refresh already cached notifications when db is empty`() = runTest { + val remoteMediator = NotificationsRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { notifications(limit = 20, excludes = emptySet()) } doReturn Response.success( + listOf( + fakeNotification(id = "5"), + fakeNotification(id = "4"), + fakeNotification(id = "3") + ) + ) + }, + db = db, + excludes = emptySet() + ) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = emptyList(), + prevKey = null, + nextKey = 0 + ) + ) + ) + + val result = remoteMediator.load(LoadType.REFRESH, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + + db.assertNotifications( + listOf( + fakeNotification(id = "5").toNotificationDataEntity(1), + fakeNotification(id = "4").toNotificationDataEntity(1), + fakeNotification(id = "3").toNotificationDataEntity(1) + ) + ) + } + + @Test + @ExperimentalPagingApi + fun `should remove deleted notification from db and keep state of statuses in the remaining ones`() = runTest { + val notificationsAlreadyInDb = listOf( + fakeNotification(id = "3"), + fakeNotification(id = "2"), + fakeNotification(id = "1") + ) + db.insert(notificationsAlreadyInDb) + + db.timelineStatusDao().setExpanded(1, "3", true) + db.timelineStatusDao().setExpanded(1, "2", true) + db.timelineStatusDao().setContentCollapsed(1, "1", false) + + val remoteMediator = NotificationsRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { notifications(limit = 20, excludes = emptySet()) } doReturn Response.success(emptyList()) + + onBlocking { notifications(maxId = "3", limit = 20, excludes = emptySet()) } doReturn Response.success( + listOf( + fakeNotification(id = "3"), + fakeNotification(id = "1") + ) + ) + }, + db = db, + excludes = emptySet() + ) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = listOf( + fakeNotification(id = "3").toNotificationDataEntity(1, isStatusExpanded = true), + fakeNotification(id = "2").toNotificationDataEntity(1, isStatusExpanded = true), + fakeNotification(id = "1").toNotificationDataEntity(1, isStatusContentShowing = true) + ), + prevKey = null, + nextKey = 0 + ) + ) + ) + + val result = remoteMediator.load(LoadType.REFRESH, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertTrue((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + + db.assertNotifications( + listOf( + fakeNotification(id = "3").toNotificationDataEntity(1, isStatusExpanded = true), + fakeNotification(id = "1").toNotificationDataEntity(1, isStatusContentShowing = true) + ) + ) + } + + @Test + @ExperimentalPagingApi + fun `should not remove placeholder in timeline`() = runTest { + val notificationsAlreadyInDb = listOf( + fakeNotification(id = "8"), + fakeNotification(id = "7"), + fakeNotification(id = "1") + ) + db.insert(notificationsAlreadyInDb) + + val placeholder = Placeholder(id = "6", loading = false).toNotificationEntity(1) + db.notificationsDao().insertNotification(placeholder) + + val remoteMediator = NotificationsRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { notifications(sinceId = "6", limit = 20, excludes = emptySet()) } doReturn Response.success( + listOf( + fakeNotification(id = "9"), + fakeNotification(id = "8"), + fakeNotification(id = "7") + ) + ) + onBlocking { notifications(maxId = "8", sinceId = "6", limit = 20, excludes = emptySet()) } doReturn Response.success( + listOf( + fakeNotification(id = "8"), + fakeNotification(id = "7") + ) + ) + }, + db = db, + excludes = emptySet() + ) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = notificationsAlreadyInDb.map { it.toNotificationDataEntity(1) }, + prevKey = null, + nextKey = 0 + ) + ) + ) + + val result = remoteMediator.load(LoadType.REFRESH, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertFalse((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + + db.assertNotifications( + listOf( + fakeNotification(id = "9").toNotificationDataEntity(1), + fakeNotification(id = "8").toNotificationDataEntity(1), + fakeNotification(id = "7").toNotificationDataEntity(1), + Placeholder(id = "6", loading = false).toNotificationDataEntity(1), + fakeNotification(id = "1").toNotificationDataEntity(1) + ) + ) + } + + @Test + @ExperimentalPagingApi + fun `should append notifications`() = runTest { + val notificationsAlreadyInDb = listOf( + fakeNotification(id = "8"), + fakeNotification(id = "7"), + fakeNotification(id = "5") + ) + + db.insert(notificationsAlreadyInDb) + + val remoteMediator = NotificationsRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { notifications(maxId = "5", limit = 20, excludes = emptySet()) } doReturn Response.success( + listOf( + fakeNotification(id = "3"), + fakeNotification(id = "2"), + fakeNotification(id = "1") + ) + ) + }, + db = db, + excludes = emptySet() + ) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = notificationsAlreadyInDb.map { it.toNotificationDataEntity(1) }, + prevKey = null, + nextKey = 0 + ) + ) + ) + + val result = remoteMediator.load(LoadType.APPEND, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + db.assertNotifications( + listOf( + fakeNotification(id = "8").toNotificationDataEntity(1), + fakeNotification(id = "7").toNotificationDataEntity(1), + fakeNotification(id = "5").toNotificationDataEntity(1), + fakeNotification(id = "3").toNotificationDataEntity(1), + fakeNotification(id = "2").toNotificationDataEntity(1), + fakeNotification(id = "1").toNotificationDataEntity(1) + ) + ) + } + + private fun state( + pages: List<PagingSource.LoadResult.Page<Int, NotificationDataEntity>> = emptyList(), + pageSize: Int = 20 + ) = PagingState( + pages = pages, + anchorPosition = null, + config = PagingConfig( + pageSize = pageSize + ), + leadingPlaceholderCount = 0 + ) +} diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt new file mode 100644 index 0000000..f003255 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/CachedTimelineRemoteMediatorTest.kt @@ -0,0 +1,526 @@ +package com.keylesspalace.tusky.components.timeline + +import android.os.Looper.getMainLooper +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingConfig +import androidx.paging.PagingSource +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.keylesspalace.tusky.components.timeline.viewmodel.CachedTimelineRemoteMediator +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.Converters +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.db.entity.HomeTimelineData +import com.keylesspalace.tusky.di.NetworkModule +import java.io.IOException +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.mock +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config +import retrofit2.HttpException +import retrofit2.Response + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class CachedTimelineRemoteMediatorTest { + + private val accountManager: AccountManager = mock { + on { activeAccount } doReturn AccountEntity( + id = 1, + domain = "mastodon.example", + accessToken = "token", + clientId = "id", + clientSecret = "secret", + isActive = true + ) + } + + private lateinit var db: AppDatabase + + private val moshi = NetworkModule.providesMoshi() + + @Before + @ExperimentalCoroutinesApi + fun setup() { + shadowOf(getMainLooper()).idle() + + val context = InstrumentationRegistry.getInstrumentation().targetContext + db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) + .addTypeConverter(Converters(moshi)) + .build() + } + + @After + @ExperimentalCoroutinesApi + fun tearDown() { + db.close() + } + + @Test + @ExperimentalPagingApi + fun `should return error when network call returns error code`() = runTest { + val remoteMediator = CachedTimelineRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody()) + }, + db = db, + ) + + val result = remoteMediator.load(LoadType.REFRESH, state()) + + assertTrue(result is RemoteMediator.MediatorResult.Error) + assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is HttpException) + assertEquals(500, (result.throwable as HttpException).code()) + } + + @Test + @ExperimentalPagingApi + fun `should return error when network call fails`() = runTest { + val remoteMediator = CachedTimelineRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { homeTimeline(anyOrNull(), anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException() + }, + db = db, + ) + + val result = remoteMediator.load(LoadType.REFRESH, state()) + + assertTrue(result is RemoteMediator.MediatorResult.Error) + assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is IOException) + } + + @Test + @ExperimentalPagingApi + fun `should not prepend statuses`() = runTest { + val remoteMediator = CachedTimelineRemoteMediator( + accountManager = accountManager, + api = mock(), + db = db, + ) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = listOf( + fakeHomeTimelineData("3") + ), + prevKey = null, + nextKey = 1 + ) + ) + ) + + val result = remoteMediator.load(LoadType.PREPEND, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertTrue((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + } + + @Test + @ExperimentalPagingApi + fun `should refresh and insert placeholder when a whole page with no overlap to existing statuses is loaded`() = runTest { + val statusesAlreadyInDb = listOf( + fakeHomeTimelineData("3"), + fakeHomeTimelineData("2"), + fakeHomeTimelineData("1") + ) + + db.insert(statusesAlreadyInDb) + + val remoteMediator = CachedTimelineRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { homeTimeline(limit = 3) } doReturn Response.success( + listOf( + fakeStatus("8"), + fakeStatus("7"), + fakeStatus("5") + ) + ) + onBlocking { homeTimeline(maxId = "3", limit = 3) } doReturn Response.success( + listOf( + fakeStatus("3"), + fakeStatus("2"), + fakeStatus("1") + ) + ) + }, + db = db, + ) + + val state = state( + pages = listOf( + PagingSource.LoadResult.Page( + data = statusesAlreadyInDb, + prevKey = null, + nextKey = 0 + ) + ), + pageSize = 3 + ) + + val result = remoteMediator.load(LoadType.REFRESH, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + + db.assertTimeline( + listOf( + fakeHomeTimelineData("8"), + fakeHomeTimelineData("7"), + fakePlaceholderHomeTimelineData("5"), + fakeHomeTimelineData("3"), + fakeHomeTimelineData("2"), + fakeHomeTimelineData("1") + ) + ) + } + + @Test + @ExperimentalPagingApi + fun `should refresh and not insert placeholder when less than a whole page is loaded`() = runTest { + val statusesAlreadyInDb = listOf( + fakeHomeTimelineData("3"), + fakeHomeTimelineData("2"), + fakeHomeTimelineData("1") + ) + + db.insert(statusesAlreadyInDb) + + val remoteMediator = CachedTimelineRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { homeTimeline(limit = 20) } doReturn Response.success( + listOf( + fakeStatus("8"), + fakeStatus("7"), + fakeStatus("5") + ) + ) + onBlocking { homeTimeline(maxId = "3", limit = 20) } doReturn Response.success( + listOf( + fakeStatus("3"), + fakeStatus("2"), + fakeStatus("1") + ) + ) + }, + db = db, + ) + + val state = state( + pages = listOf( + PagingSource.LoadResult.Page( + data = statusesAlreadyInDb, + prevKey = null, + nextKey = 0 + ) + ) + ) + + val result = remoteMediator.load(LoadType.REFRESH, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + + db.assertTimeline( + listOf( + fakeHomeTimelineData("8"), + fakeHomeTimelineData("7"), + fakeHomeTimelineData("5"), + fakeHomeTimelineData("3"), + fakeHomeTimelineData("2"), + fakeHomeTimelineData("1") + ) + ) + } + + @Test + @ExperimentalPagingApi + fun `should refresh and not insert placeholders when there is overlap with existing statuses`() = runTest { + val statusesAlreadyInDb = listOf( + fakeHomeTimelineData("3"), + fakeHomeTimelineData("2"), + fakeHomeTimelineData("1") + ) + + db.insert(statusesAlreadyInDb) + + val remoteMediator = CachedTimelineRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { homeTimeline(limit = 3) } doReturn Response.success( + listOf( + fakeStatus("6"), + fakeStatus("4"), + fakeStatus("3") + ) + ) + onBlocking { homeTimeline(maxId = "3", limit = 3) } doReturn Response.success( + listOf( + fakeStatus("3"), + fakeStatus("2"), + fakeStatus("1") + ) + ) + }, + db = db, + ) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = statusesAlreadyInDb, + prevKey = null, + nextKey = 0 + ) + ), + pageSize = 3 + ) + + val result = remoteMediator.load(LoadType.REFRESH, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + + db.assertTimeline( + listOf( + fakeHomeTimelineData("6"), + fakeHomeTimelineData("4"), + fakeHomeTimelineData("3"), + fakeHomeTimelineData("2"), + fakeHomeTimelineData("1") + ) + ) + } + + @Test + @ExperimentalPagingApi + fun `should not try to refresh already cached statuses when db is empty`() = runTest { + val remoteMediator = CachedTimelineRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { homeTimeline(limit = 20) } doReturn Response.success( + listOf( + fakeStatus("5"), + fakeStatus("4"), + fakeStatus("3") + ) + ) + }, + db = db, + ) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = emptyList(), + prevKey = null, + nextKey = 0 + ) + ) + ) + + val result = remoteMediator.load(LoadType.REFRESH, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + + db.assertTimeline( + listOf( + fakeHomeTimelineData("5"), + fakeHomeTimelineData("4"), + fakeHomeTimelineData("3") + ) + ) + } + + @Test + @ExperimentalPagingApi + fun `should remove deleted status from db and keep state of other cached statuses`() = runTest { + val statusesAlreadyInDb = listOf( + fakeHomeTimelineData("3", expanded = true), + fakeHomeTimelineData("2"), + fakeHomeTimelineData("1", expanded = false) + ) + + db.insert(statusesAlreadyInDb) + + val remoteMediator = CachedTimelineRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { homeTimeline(limit = 20) } doReturn Response.success(emptyList()) + + onBlocking { homeTimeline(maxId = "3", limit = 20) } doReturn Response.success( + listOf( + fakeStatus("3"), + fakeStatus("1") + ) + ) + }, + db = db, + ) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = statusesAlreadyInDb, + prevKey = null, + nextKey = 0 + ) + ) + ) + + val result = remoteMediator.load(LoadType.REFRESH, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertTrue((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + + db.assertTimeline( + listOf( + fakeHomeTimelineData("3", expanded = true), + fakeHomeTimelineData("1", expanded = false) + ) + ) + } + + @Test + @ExperimentalPagingApi + fun `should not remove placeholder in timeline`() = runTest { + val statusesAlreadyInDb = listOf( + fakeHomeTimelineData("8"), + fakeHomeTimelineData("7"), + fakePlaceholderHomeTimelineData("6"), + fakeHomeTimelineData("1") + ) + + db.insert(statusesAlreadyInDb) + + val remoteMediator = CachedTimelineRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { homeTimeline(sinceId = "6", limit = 20) } doReturn Response.success( + listOf( + fakeStatus("9"), + fakeStatus("8"), + fakeStatus("7") + ) + ) + onBlocking { homeTimeline(maxId = "8", sinceId = "6", limit = 20) } doReturn Response.success( + listOf( + fakeStatus("8"), + fakeStatus("7") + ) + ) + }, + db = db, + ) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = statusesAlreadyInDb, + prevKey = null, + nextKey = 0 + ) + ) + ) + + val result = remoteMediator.load(LoadType.REFRESH, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertFalse((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + + db.assertTimeline( + listOf( + fakeHomeTimelineData("9"), + fakeHomeTimelineData("8"), + fakeHomeTimelineData("7"), + fakePlaceholderHomeTimelineData("6"), + fakeHomeTimelineData("1") + ) + ) + } + + @Test + @ExperimentalPagingApi + fun `should append statuses`() = runTest { + val statusesAlreadyInDb = listOf( + fakeHomeTimelineData("8"), + fakeHomeTimelineData("7"), + fakeHomeTimelineData("5") + ) + + db.insert(statusesAlreadyInDb) + + val remoteMediator = CachedTimelineRemoteMediator( + accountManager = accountManager, + api = mock { + onBlocking { homeTimeline(maxId = "5", limit = 20) } doReturn Response.success( + listOf( + fakeStatus("3"), + fakeStatus("2"), + fakeStatus("1") + ) + ) + }, + db = db, + ) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = statusesAlreadyInDb, + prevKey = null, + nextKey = 0 + ) + ) + ) + + val result = remoteMediator.load(LoadType.APPEND, state) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + db.assertTimeline( + listOf( + fakeHomeTimelineData("8"), + fakeHomeTimelineData("7"), + fakeHomeTimelineData("5"), + fakeHomeTimelineData("3"), + fakeHomeTimelineData("2"), + fakeHomeTimelineData("1") + ) + ) + } + + private fun state( + pages: List<PagingSource.LoadResult.Page<Int, HomeTimelineData>> = emptyList(), + pageSize: Int = 20 + ) = PagingState( + pages = pages, + anchorPosition = null, + config = PagingConfig( + pageSize = pageSize + ), + leadingPlaceholderCount = 0 + ) +} diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt new file mode 100644 index 0000000..5535872 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelinePagingSourceTest.kt @@ -0,0 +1,64 @@ +package com.keylesspalace.tusky.components.timeline + +import androidx.paging.PagingSource +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelinePagingSource +import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel +import kotlinx.coroutines.runBlocking +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.robolectric.annotation.Config + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class NetworkTimelinePagingSourceTest { + + private val status = fakeStatusViewData() + + private val timelineViewModel: NetworkTimelineViewModel = mock { + on { statusData } doReturn mutableListOf(status) + } + + @Test + fun `should return empty list when params are Append`() { + val pagingSource = NetworkTimelinePagingSource(timelineViewModel) + + val params = PagingSource.LoadParams.Append("132", 20, false) + + val expectedResult = PagingSource.LoadResult.Page(emptyList(), null, null) + + runBlocking { + assertEquals(expectedResult, pagingSource.load(params)) + } + } + + @Test + fun `should return empty list when params are Prepend`() { + val pagingSource = NetworkTimelinePagingSource(timelineViewModel) + + val params = PagingSource.LoadParams.Prepend("132", 20, false) + + val expectedResult = PagingSource.LoadResult.Page(emptyList(), null, null) + + runBlocking { + assertEquals(expectedResult, pagingSource.load(params)) + } + } + + @Test + fun `should return full list when params are Refresh`() { + val pagingSource = NetworkTimelinePagingSource(timelineViewModel) + + val params = PagingSource.LoadParams.Refresh<String>(null, 20, false) + + val expectedResult = PagingSource.LoadResult.Page(listOf(status), null, null) + + runBlocking { + val result = pagingSource.load(params) + assertEquals(expectedResult, result) + } + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt new file mode 100644 index 0000000..14621fa --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/NetworkTimelineRemoteMediatorTest.kt @@ -0,0 +1,447 @@ +package com.keylesspalace.tusky.components.timeline + +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingConfig +import androidx.paging.PagingSource +import androidx.paging.PagingState +import androidx.paging.RemoteMediator +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineRemoteMediator +import com.keylesspalace.tusky.components.timeline.viewmodel.NetworkTimelineViewModel +import com.keylesspalace.tusky.components.timeline.viewmodel.TimelineViewModel +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.viewdata.StatusViewData +import java.io.IOException +import kotlinx.coroutines.runBlocking +import okhttp3.Headers +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.robolectric.annotation.Config +import retrofit2.HttpException +import retrofit2.Response + +@Config(sdk = [29]) +@RunWith(AndroidJUnit4::class) +class NetworkTimelineRemoteMediatorTest { + + private val accountManager: AccountManager = mock { + on { activeAccount } doReturn AccountEntity( + id = 1, + domain = "mastodon.example", + accessToken = "token", + clientId = "id", + clientSecret = "secret", + isActive = true + ) + } + + @Test + @ExperimentalPagingApi + fun `should return error when network call returns error code`() { + val timelineViewModel: NetworkTimelineViewModel = mock { + on { statusData } doReturn mutableListOf() + onBlocking { fetchStatusesForKind(anyOrNull(), anyOrNull(), anyOrNull()) } doReturn Response.error(500, "".toResponseBody()) + } + + val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel) + + val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) } + + assertTrue(result is RemoteMediator.MediatorResult.Error) + assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is HttpException) + assertEquals(500, (result.throwable as HttpException).code()) + } + + @Test + @ExperimentalPagingApi + fun `should return error when network call fails`() { + val timelineViewModel: NetworkTimelineViewModel = mock { + on { statusData } doReturn mutableListOf() + onBlocking { fetchStatusesForKind(anyOrNull(), anyOrNull(), anyOrNull()) } doThrow IOException() + } + + val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel) + + val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state()) } + + assertTrue(result is RemoteMediator.MediatorResult.Error) + assertTrue((result as RemoteMediator.MediatorResult.Error).throwable is IOException) + } + + @Test + @ExperimentalPagingApi + fun `should do initial loading`() { + val statuses: MutableList<StatusViewData> = mutableListOf() + + val timelineViewModel: NetworkTimelineViewModel = mock { + on { statusData } doReturn statuses + on { nextKey } doReturn null + onBlocking { fetchStatusesForKind(null, null, 20) } doReturn Response.success( + listOf( + fakeStatus("7"), + fakeStatus("6"), + fakeStatus("5") + ), + Headers.headersOf( + "Link", + "<https://mastodon.example/api/v1/favourites?limit=20&max_id=4>; rel=\"next\", <https://mastodon.example/api/v1/favourites?limit=20&min_id=8>; rel=\"prev\"" + ) + ) + } + + val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = emptyList(), + prevKey = null, + nextKey = null + ) + ) + ) + + val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) } + + val newStatusData = mutableListOf( + fakeStatusViewData("7"), + fakeStatusViewData("6"), + fakeStatusViewData("5") + ) + + verify(timelineViewModel).nextKey = "4" + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + assertEquals(newStatusData, statuses) + } + + @Test + @ExperimentalPagingApi + fun `should not prepend statuses`() { + val statuses: MutableList<StatusViewData> = mutableListOf( + fakeStatusViewData("3"), + fakeStatusViewData("2"), + fakeStatusViewData("1") + ) + + val timelineViewModel: NetworkTimelineViewModel = mock { + on { statusData } doReturn statuses + on { nextKey } doReturn "0" + onBlocking { fetchStatusesForKind(null, null, 20) } doReturn Response.success( + listOf( + fakeStatus("5"), + fakeStatus("4"), + fakeStatus("3") + ) + ) + } + + val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = listOf( + fakeStatusViewData("3"), + fakeStatusViewData("2"), + fakeStatusViewData("1") + ), + prevKey = null, + nextKey = "0" + ) + ) + ) + + val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) } + + val newStatusData = mutableListOf( + fakeStatusViewData("5"), + fakeStatusViewData("4"), + fakeStatusViewData("3"), + fakeStatusViewData("2"), + fakeStatusViewData("1") + ) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + assertEquals(newStatusData, statuses) + } + + @Test + @ExperimentalPagingApi + fun `should refresh and insert placeholder`() { + val statuses: MutableList<StatusViewData> = mutableListOf( + fakeStatusViewData("3"), + fakeStatusViewData("2"), + fakeStatusViewData("1") + ) + + val timelineViewModel: NetworkTimelineViewModel = mock { + on { statusData } doReturn statuses + on { nextKey } doReturn "0" + onBlocking { fetchStatusesForKind(null, null, 20) } doReturn Response.success( + listOf( + fakeStatus("10"), + fakeStatus("9"), + fakeStatus("7") + ) + ) + } + + val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = listOf( + fakeStatusViewData("3"), + fakeStatusViewData("2"), + fakeStatusViewData("1") + ), + prevKey = null, + nextKey = "0" + ) + ) + ) + + val result = runBlocking { remoteMediator.load(LoadType.REFRESH, state) } + + val newStatusData = mutableListOf( + fakeStatusViewData("10"), + fakeStatusViewData("9"), + StatusViewData.Placeholder("7", false), + fakeStatusViewData("3"), + fakeStatusViewData("2"), + fakeStatusViewData("1") + ) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + assertEquals(newStatusData, statuses) + } + + @Test + @ExperimentalPagingApi + fun `should refresh and not insert placeholders`() { + val statuses: MutableList<StatusViewData> = mutableListOf( + fakeStatusViewData("8"), + fakeStatusViewData("7"), + fakeStatusViewData("5") + ) + + val timelineViewModel: NetworkTimelineViewModel = mock { + on { statusData } doReturn statuses + on { nextKey } doReturn "3" + onBlocking { fetchStatusesForKind("3", null, 20) } doReturn Response.success( + listOf( + fakeStatus("3"), + fakeStatus("2"), + fakeStatus("1") + ) + ) + } + + val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = listOf( + fakeStatusViewData("8"), + fakeStatusViewData("7"), + fakeStatusViewData("5") + ), + prevKey = null, + nextKey = "3" + ) + ) + ) + + val result = runBlocking { remoteMediator.load(LoadType.APPEND, state) } + + val newStatusData = mutableListOf( + fakeStatusViewData("8"), + fakeStatusViewData("7"), + fakeStatusViewData("5"), + fakeStatusViewData("3"), + fakeStatusViewData("2"), + fakeStatusViewData("1") + ) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + assertEquals(newStatusData, statuses) + } + + @Test + @ExperimentalPagingApi + fun `should append statuses`() { + val statuses: MutableList<StatusViewData> = mutableListOf( + fakeStatusViewData("8"), + fakeStatusViewData("7"), + fakeStatusViewData("5") + ) + + val timelineViewModel: NetworkTimelineViewModel = mock { + on { statusData } doReturn statuses + on { nextKey } doReturn "3" + onBlocking { fetchStatusesForKind("3", null, 20) } doReturn Response.success( + listOf( + fakeStatus("3"), + fakeStatus("2"), + fakeStatus("1") + ), + Headers.headersOf( + "Link", + "<https://mastodon.example/api/v1/favourites?limit=20&max_id=0>; rel=\"next\", <https://mastodon.example/api/v1/favourites?limit=20&min_id=4>; rel=\"prev\"" + ) + ) + } + + val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = listOf( + fakeStatusViewData("8"), + fakeStatusViewData("7"), + fakeStatusViewData("5") + ), + prevKey = null, + nextKey = "3" + ) + ) + ) + + val result = runBlocking { remoteMediator.load(LoadType.APPEND, state) } + + val newStatusData = mutableListOf( + fakeStatusViewData("8"), + fakeStatusViewData("7"), + fakeStatusViewData("5"), + fakeStatusViewData("3"), + fakeStatusViewData("2"), + fakeStatusViewData("1") + ) + verify(timelineViewModel).nextKey = "0" + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + assertEquals(newStatusData, statuses) + } + + @Test + @ExperimentalPagingApi + fun `should not append statuses when pagination end has been reached`() { + val statuses: MutableList<StatusViewData> = mutableListOf( + fakeStatusViewData("8"), + fakeStatusViewData("7"), + fakeStatusViewData("5") + ) + + val timelineViewModel: NetworkTimelineViewModel = mock { + on { statusData } doReturn statuses + on { nextKey } doReturn null + } + + val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = listOf( + fakeStatusViewData("8"), + fakeStatusViewData("7"), + fakeStatusViewData("5") + ), + prevKey = null, + nextKey = null + ) + ) + ) + + val result = runBlocking { remoteMediator.load(LoadType.APPEND, state) } + + val newStatusData = mutableListOf( + fakeStatusViewData("8"), + fakeStatusViewData("7"), + fakeStatusViewData("5") + ) + + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertTrue((result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + assertEquals(newStatusData, statuses) + } + + @Test + @ExperimentalPagingApi + fun `should not append duplicates for trending statuses`() { + val statuses: MutableList<StatusViewData> = mutableListOf( + fakeStatusViewData("5"), + fakeStatusViewData("4"), + fakeStatusViewData("3") + ) + + val timelineViewModel: NetworkTimelineViewModel = mock { + on { statusData } doReturn statuses + on { nextKey } doReturn "3" + on { kind } doReturn TimelineViewModel.Kind.PUBLIC_TRENDING_STATUSES + onBlocking { fetchStatusesForKind("3", null, 20) } doReturn Response.success( + listOf( + fakeStatus("3"), + fakeStatus("2"), + fakeStatus("1") + ), + Headers.headersOf( + "Link", + "<https://mastodon.example/api/v1/trends/statuses?offset=5>; rel=\"next\"" + ) + ) + } + + val remoteMediator = NetworkTimelineRemoteMediator(accountManager, timelineViewModel) + + val state = state( + listOf( + PagingSource.LoadResult.Page( + data = statuses, + prevKey = null, + nextKey = "3" + ) + ) + ) + + val result = runBlocking { remoteMediator.load(LoadType.APPEND, state) } + + val newStatusData = mutableListOf( + fakeStatusViewData("5"), + fakeStatusViewData("4"), + fakeStatusViewData("3"), + fakeStatusViewData("2"), + fakeStatusViewData("1") + ) + verify(timelineViewModel).nextKey = "5" + assertTrue(result is RemoteMediator.MediatorResult.Success) + assertEquals(false, (result as RemoteMediator.MediatorResult.Success).endOfPaginationReached) + assertEquals(newStatusData, statuses) + } + + private fun state(pages: List<PagingSource.LoadResult.Page<String, StatusViewData>> = emptyList()) = PagingState( + pages = pages, + anchorPosition = null, + config = PagingConfig( + pageSize = 20 + ), + leadingPlaceholderCount = 0 + ) +} diff --git a/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineFaker.kt b/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineFaker.kt new file mode 100644 index 0000000..2351347 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/components/timeline/TimelineFaker.kt @@ -0,0 +1,189 @@ +package com.keylesspalace.tusky.components.timeline + +import androidx.paging.PagingSource +import androidx.room.withTransaction +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.entity.HomeTimelineData +import com.keylesspalace.tusky.db.entity.HomeTimelineEntity +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.entity.TimelineAccount +import com.keylesspalace.tusky.viewdata.StatusViewData +import java.util.Date +import org.junit.Assert.assertEquals + +private val fixedDate = Date(1638889052000) + +fun fakeAccount( + id: String = "100", + domain: String = "mastodon.example" +) = TimelineAccount( + id = id, + localUsername = "connyduck", + username = "connyduck@$domain", + displayName = "Conny Duck", + note = "This is their bio", + url = "https://$domain/@ConnyDuck", + avatar = "https://$domain/system/accounts/avatars/000/150/486/original/ab27d7ddd18a10ea.jpg" +) + +fun fakeStatus( + id: String = "100", + authorServerId: String = "100", + inReplyToId: String? = null, + inReplyToAccountId: String? = null, + spoilerText: String = "", + reblogged: Boolean = false, + favourited: Boolean = true, + bookmarked: Boolean = true, + domain: String = "mastodon.example" +) = Status( + id = id, + url = "https://$domain/@ConnyDuck/$id", + account = fakeAccount( + id = authorServerId, + domain = domain + ), + inReplyToId = inReplyToId, + inReplyToAccountId = inReplyToAccountId, + reblog = null, + content = "Test", + createdAt = fixedDate, + editedAt = null, + emojis = emptyList(), + reblogsCount = 1, + favouritesCount = 2, + repliesCount = 3, + reblogged = reblogged, + favourited = favourited, + bookmarked = bookmarked, + sensitive = true, + spoilerText = spoilerText, + visibility = Status.Visibility.PUBLIC, + attachments = ArrayList(), + mentions = emptyList(), + tags = emptyList(), + application = Status.Application("Tusky", "https://tusky.app"), + pinned = false, + muted = false, + poll = null, + card = null, + language = null, + filtered = emptyList() +) + +fun fakeStatusViewData( + id: String = "100", + inReplyToId: String? = null, + inReplyToAccountId: String? = null, + isDetailed: Boolean = false, + spoilerText: String = "", + isExpanded: Boolean = false, + isShowingContent: Boolean = false, + isCollapsed: Boolean = !isDetailed, + reblogged: Boolean = false, + favourited: Boolean = true, + bookmarked: Boolean = true +) = StatusViewData.Concrete( + status = fakeStatus( + id = id, + inReplyToId = inReplyToId, + inReplyToAccountId = inReplyToAccountId, + spoilerText = spoilerText, + reblogged = reblogged, + favourited = favourited, + bookmarked = bookmarked + ), + isExpanded = isExpanded, + isShowingContent = isShowingContent, + isCollapsed = isCollapsed, + isDetailed = isDetailed +) + +fun fakeHomeTimelineData( + id: String = "100", + statusId: String = id, + tuskyAccountId: Long = 1, + authorServerId: String = "100", + expanded: Boolean = false, + domain: String = "mastodon.example", + reblogAuthorServerId: String? = null +): HomeTimelineData { + val mockedStatus = fakeStatus( + id = statusId, + authorServerId = authorServerId, + domain = domain + ) + + return HomeTimelineData( + id = id, + status = mockedStatus.toEntity( + tuskyAccountId = tuskyAccountId, + expanded = expanded, + contentShowing = false, + contentCollapsed = true + ), + account = mockedStatus.account.toEntity( + tuskyAccountId = tuskyAccountId, + ), + reblogAccount = reblogAuthorServerId?.let { reblogAuthorId -> + fakeAccount( + id = reblogAuthorId + ).toEntity( + tuskyAccountId = tuskyAccountId, + ) + }, + loading = false + ) +} + +fun fakePlaceholderHomeTimelineData( + id: String +) = HomeTimelineData( + id = id, + account = null, + status = null, + reblogAccount = null, + loading = false +) + +suspend fun AppDatabase.insert(timelineItems: List<HomeTimelineData>, tuskyAccountId: Long = 1) = withTransaction { + timelineItems.forEach { timelineItem -> + timelineItem.account?.let { account -> + timelineAccountDao().insert(account) + } + timelineItem.reblogAccount?.let { account -> + timelineAccountDao().insert(account) + } + timelineItem.status?.let { status -> + timelineStatusDao().insert(status) + } + timelineDao().insertHomeTimelineItem( + HomeTimelineEntity( + tuskyAccountId = tuskyAccountId, + id = timelineItem.id, + statusId = timelineItem.status?.serverId, + reblogAccountId = timelineItem.reblogAccount?.serverId, + loading = timelineItem.loading + ) + ) + } +} + +suspend fun AppDatabase.assertTimeline( + expected: List<HomeTimelineData>, + tuskyAccountId: Long = 1 +) { + val pagingSource = timelineDao().getHomeTimeline(tuskyAccountId) + + val loadResult = pagingSource.load(PagingSource.LoadParams.Refresh(null, 100, false)) + + val loadedStatuses = (loadResult as PagingSource.LoadResult.Page).data + + assertEquals(expected.size, loadedStatuses.size) + + for ((exp, prov) in expected.zip(loadedStatuses)) { + assertEquals(exp.status, prov.status) + assertEquals(exp.account, prov.account) + assertEquals(exp.reblogAccount, prov.reblogAccount) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModelTest.kt b/app/src/test/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModelTest.kt new file mode 100644 index 0000000..6854686 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/components/viewthread/ViewThreadViewModelTest.kt @@ -0,0 +1,357 @@ +package com.keylesspalace.tusky.components.viewthread + +import android.os.Looper.getMainLooper +import androidx.arch.core.executor.testing.InstantTaskExecutorRule +import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import at.connyduck.calladapter.networkresult.NetworkResult +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.StatusChangedEvent +import com.keylesspalace.tusky.components.timeline.fakeStatus +import com.keylesspalace.tusky.components.timeline.fakeStatusViewData +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.Converters +import com.keylesspalace.tusky.db.entity.AccountEntity +import com.keylesspalace.tusky.di.NetworkModule +import com.keylesspalace.tusky.entity.StatusContext +import com.keylesspalace.tusky.network.FilterModel +import com.keylesspalace.tusky.network.MastodonApi +import com.keylesspalace.tusky.usecase.TimelineCases +import java.io.IOException +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.runBlocking +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub +import org.robolectric.Shadows.shadowOf +import org.robolectric.annotation.Config + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class ViewThreadViewModelTest { + + private lateinit var api: MastodonApi + private lateinit var eventHub: EventHub + private lateinit var viewModel: ViewThreadViewModel + private lateinit var db: AppDatabase + + private val threadId = "1234" + private val moshi = NetworkModule.providesMoshi() + + /** + * Execute each task synchronously. + * + * If you do not do this, and you have code like this under test: + * + * ``` + * fun someFunc() = viewModelScope.launch { + * _uiState.value = "initial value" + * // ... + * call_a_suspend_fun() + * // ... + * _uiState.value = "new value" + * } + * ``` + * + * and a test like: + * + * ``` + * someFunc() + * assertEquals("new value", viewModel.uiState.value) + * ``` + * + * The test will fail, because someFunc() yields at the `call_a_suspend_func()` point, + * and control returns to the test before `_uiState.value` has been changed. + */ + @get:Rule + val instantTaskRule = InstantTaskExecutorRule() + + @Before + fun setup() { + shadowOf(getMainLooper()).idle() + + api = mock { + onBlocking { getFilters() } doReturn NetworkResult.success(emptyList()) + } + eventHub = EventHub() + val filterModel = FilterModel() + val timelineCases = TimelineCases(api, eventHub) + val accountManager: AccountManager = mock { + on { activeAccount } doReturn AccountEntity( + id = 1, + domain = "mastodon.test", + accessToken = "fakeToken", + clientId = "fakeId", + clientSecret = "fakeSecret", + isActive = true + ) + } + val context = InstrumentationRegistry.getInstrumentation().targetContext + db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) + .addTypeConverter(Converters(moshi)) + .allowMainThreadQueries() + .build() + + viewModel = ViewThreadViewModel(api, filterModel, timelineCases, eventHub, accountManager, db, moshi) + } + + @After + fun closeDb() { + db.close() + } + + @Test + fun `should emit status and context when both load`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + + runBlocking { + assertEquals( + ThreadUiState.Success( + statusViewData = listOf( + fakeStatusViewData(id = "1", spoilerText = "Test"), + fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test"), + fakeStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test") + ), + detailedStatusPosition = 1, + revealButton = RevealButtonState.REVEAL + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should emit status even if context fails to load`() { + api.stub { + onBlocking { status(threadId) } doReturn NetworkResult.success(fakeStatus(id = "2", inReplyToId = "1", inReplyToAccountId = "1")) + onBlocking { statusContext(threadId) } doReturn NetworkResult.failure(IOException()) + } + + viewModel.loadThread(threadId) + + runBlocking { + assertEquals( + ThreadUiState.Success( + statusViewData = listOf( + fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true) + ), + detailedStatusPosition = 0, + revealButton = RevealButtonState.NO_BUTTON + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should emit error when status and context fail to load`() { + api.stub { + onBlocking { status(threadId) } doReturn NetworkResult.failure(IOException()) + onBlocking { statusContext(threadId) } doReturn NetworkResult.failure(IOException()) + } + + viewModel.loadThread(threadId) + + runBlocking { + assertEquals( + ThreadUiState.Error::class.java, + viewModel.uiState.first().javaClass + ) + } + } + + @Test + fun `should emit error when status fails to load`() { + api.stub { + onBlocking { status(threadId) } doReturn NetworkResult.failure(IOException()) + onBlocking { statusContext(threadId) } doReturn NetworkResult.success( + StatusContext( + ancestors = listOf(fakeStatus(id = "1")), + descendants = listOf(fakeStatus(id = "3", inReplyToId = "2", inReplyToAccountId = "1")) + ) + ) + } + + viewModel.loadThread(threadId) + + runBlocking { + assertEquals( + ThreadUiState.Error::class.java, + viewModel.uiState.first().javaClass + ) + } + } + + @Test + fun `should update state when reveal button is toggled`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + viewModel.toggleRevealButton() + + runBlocking { + assertEquals( + ThreadUiState.Success( + statusViewData = listOf( + fakeStatusViewData(id = "1", spoilerText = "Test", isExpanded = true), + fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isExpanded = true), + fakeStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test", isExpanded = true) + ), + detailedStatusPosition = 1, + revealButton = RevealButtonState.HIDE + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should handle status changed event`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + + runBlocking { + eventHub.dispatch(StatusChangedEvent(fakeStatus(id = "1", spoilerText = "Test", favourited = false))) + + assertEquals( + ThreadUiState.Success( + statusViewData = listOf( + fakeStatusViewData(id = "1", spoilerText = "Test", favourited = false), + fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test"), + fakeStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test") + ), + detailedStatusPosition = 1, + revealButton = RevealButtonState.REVEAL + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should remove status`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + + viewModel.removeStatus(fakeStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")) + + runBlocking { + assertEquals( + ThreadUiState.Success( + statusViewData = listOf( + fakeStatusViewData(id = "1", spoilerText = "Test"), + fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test") + ), + detailedStatusPosition = 1, + revealButton = RevealButtonState.REVEAL + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should change status expanded state`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + + viewModel.changeExpanded( + true, + fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test") + ) + + runBlocking { + assertEquals( + ThreadUiState.Success( + statusViewData = listOf( + fakeStatusViewData(id = "1", spoilerText = "Test"), + fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isExpanded = true), + fakeStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test") + ), + detailedStatusPosition = 1, + revealButton = RevealButtonState.REVEAL + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should change content collapsed state`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + + viewModel.changeContentCollapsed( + true, + fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test") + ) + + runBlocking { + assertEquals( + ThreadUiState.Success( + statusViewData = listOf( + fakeStatusViewData(id = "1", spoilerText = "Test"), + fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isCollapsed = true), + fakeStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test") + ), + detailedStatusPosition = 1, + revealButton = RevealButtonState.REVEAL + ), + viewModel.uiState.first() + ) + } + } + + @Test + fun `should change content showing state`() { + mockSuccessResponses() + + viewModel.loadThread(threadId) + + viewModel.changeContentShowing( + true, + fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test") + ) + + runBlocking { + assertEquals( + ThreadUiState.Success( + statusViewData = listOf( + fakeStatusViewData(id = "1", spoilerText = "Test"), + fakeStatusViewData(id = "2", inReplyToId = "1", inReplyToAccountId = "1", isDetailed = true, spoilerText = "Test", isShowingContent = true), + fakeStatusViewData(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test") + ), + detailedStatusPosition = 1, + revealButton = RevealButtonState.REVEAL + ), + viewModel.uiState.first() + ) + } + } + + private fun mockSuccessResponses() { + api.stub { + onBlocking { status(threadId) } doReturn NetworkResult.success(fakeStatus(id = "2", inReplyToId = "1", inReplyToAccountId = "1", spoilerText = "Test")) + onBlocking { statusContext(threadId) } doReturn NetworkResult.success( + StatusContext( + ancestors = listOf(fakeStatus(id = "1", spoilerText = "Test")), + descendants = listOf(fakeStatus(id = "3", inReplyToId = "2", inReplyToAccountId = "1", spoilerText = "Test")) + ) + ) + } + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/db/dao/DatabaseCleanerTest.kt b/app/src/test/java/com/keylesspalace/tusky/db/dao/DatabaseCleanerTest.kt new file mode 100644 index 0000000..8321e7e --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/db/dao/DatabaseCleanerTest.kt @@ -0,0 +1,229 @@ +package com.keylesspalace.tusky.db.dao + +import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.keylesspalace.tusky.components.notifications.fakeNotification +import com.keylesspalace.tusky.components.notifications.fakeReport +import com.keylesspalace.tusky.components.notifications.insert +import com.keylesspalace.tusky.components.timeline.fakeAccount +import com.keylesspalace.tusky.components.timeline.fakeHomeTimelineData +import com.keylesspalace.tusky.components.timeline.fakeStatus +import com.keylesspalace.tusky.components.timeline.insert +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.Converters +import com.keylesspalace.tusky.db.DatabaseCleaner +import com.keylesspalace.tusky.db.entity.HomeTimelineEntity +import com.keylesspalace.tusky.db.entity.NotificationEntity +import com.keylesspalace.tusky.db.entity.NotificationReportEntity +import com.keylesspalace.tusky.db.entity.TimelineAccountEntity +import com.keylesspalace.tusky.db.entity.TimelineStatusEntity +import com.keylesspalace.tusky.di.NetworkModule +import kotlin.reflect.KClass +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class DatabaseCleanerTest { + private lateinit var timelineDao: TimelineDao + private lateinit var dbCleaner: DatabaseCleaner + private lateinit var db: AppDatabase + + private val moshi = NetworkModule.providesMoshi() + + @Before + fun createDb() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) + .addTypeConverter(Converters(moshi)) + .allowMainThreadQueries() + .build() + timelineDao = db.timelineDao() + dbCleaner = DatabaseCleaner(db) + } + + @After + fun closeDb() { + db.close() + } + + @Test + fun cleanupOldData() = runTest { + fillDatabase() + + dbCleaner.cleanupOldData(tuskyAccountId = 1, timelineLimit = 3, notificationLimit = 3) + + // all but 3 timeline items and notifications and all references items should be gone for Tusky account 1 + // items of Tusky account 2 should be untouched + expect( + hometimelineItems = listOf( + 1L to "10", + 1L to "100", + 1L to "8", + 2L to "2" + ), + statuses = listOf( + 1L to "10", + 1L to "100", + 1L to "8", + 1L to "n3", + 1L to "n4", + 1L to "n5", + 2L to "2", + 2L to "n1", + 2L to "n2", + 2L to "n3", + 2L to "n4" + ), + notifications = listOf( + 1L to "3", + 1L to "4", + 1L to "5", + 2L to "1", + 2L to "2", + 2L to "3", + 2L to "4", + ), + accounts = listOf( + 1L to "10", + 1L to "100", + 1L to "3", + 1L to "R10", + 1L to "n3", + 1L to "n4", + 1L to "n5", + 1L to "r2", + 2L to "100", + 2L to "5", + 2L to "n1", + 2L to "n2", + 2L to "n3", + 2L to "n4", + 2L to "r1" + ), + reports = listOf( + 1L to "2", + 2L to "1" + ), + ) + } + + @Test + fun cleanupEverything() = runTest { + fillDatabase() + + dbCleaner.cleanupEverything(tuskyAccountId = 1) + + // everything from Tusky account 1 should be gone + // items of Tusky account 2 should be untouched + expect( + hometimelineItems = listOf( + 2L to "2" + ), + statuses = listOf( + 2L to "2", + 2L to "n1", + 2L to "n2", + 2L to "n3", + 2L to "n4" + ), + notifications = listOf( + 2L to "1", + 2L to "2", + 2L to "3", + 2L to "4", + ), + accounts = listOf( + 2L to "100", + 2L to "5", + 2L to "n1", + 2L to "n2", + 2L to "n3", + 2L to "n4", + 2L to "r1" + ), + reports = listOf( + 2L to "1" + ), + ) + } + + private suspend fun fillDatabase() { + db.insert( + listOf( + fakeHomeTimelineData(id = "100", authorServerId = "100"), + fakeHomeTimelineData(id = "10", authorServerId = "3"), + fakeHomeTimelineData(id = "8", reblogAuthorServerId = "R10", authorServerId = "10"), + fakeHomeTimelineData(id = "5", authorServerId = "100"), + fakeHomeTimelineData(id = "3", authorServerId = "4"), + fakeHomeTimelineData(id = "1", authorServerId = "5") + ), + tuskyAccountId = 1 + ) + db.insert( + listOf( + fakeHomeTimelineData(id = "2", tuskyAccountId = 2, authorServerId = "5") + ), + tuskyAccountId = 2 + ) + + db.insert( + listOf( + fakeNotification(id = "1", account = fakeAccount(id = "n1"), status = fakeStatus(id = "n1")), + fakeNotification(id = "2", account = fakeAccount(id = "n2"), status = fakeStatus(id = "n2"), report = fakeReport(targetAccount = fakeAccount(id = "r1"))), + fakeNotification(id = "3", account = fakeAccount(id = "n3"), status = fakeStatus(id = "n3")), + fakeNotification(id = "4", account = fakeAccount(id = "n4"), status = fakeStatus(id = "n4"), report = fakeReport(id = "2", targetAccount = fakeAccount(id = "r2"))), + fakeNotification(id = "5", account = fakeAccount(id = "n5"), status = fakeStatus(id = "n5")), + ), + tuskyAccountId = 1 + ) + db.insert( + listOf( + fakeNotification(id = "1", account = fakeAccount(id = "n1"), status = fakeStatus(id = "n1")), + fakeNotification(id = "2", account = fakeAccount(id = "n2"), status = fakeStatus(id = "n2")), + fakeNotification(id = "3", account = fakeAccount(id = "n3"), status = fakeStatus(id = "n3")), + fakeNotification(id = "4", account = fakeAccount(id = "n4"), status = fakeStatus(id = "n4"), report = fakeReport(targetAccount = fakeAccount(id = "r1"))) + ), + tuskyAccountId = 2 + ) + } + + private fun expect( + hometimelineItems: List<Pair<Long, String>>, + statuses: List<Pair<Long, String>>, + notifications: List<Pair<Long, String>>, + accounts: List<Pair<Long, String>>, + reports: List<Pair<Long, String>>, + ) { + expect(HomeTimelineEntity::class, "id", hometimelineItems) + expect(TimelineStatusEntity::class, "serverId", statuses) + expect(NotificationEntity::class, "id", notifications) + expect(TimelineAccountEntity::class, "serverId", accounts) + expect(NotificationReportEntity::class, "serverId", reports) + } + + private fun expect( + entity: KClass<*>, + idName: String, + expectedItems: List<Pair<Long, String>> + ) { + val loadedItems: MutableList<Pair<Long, String>> = mutableListOf() + val cursor = db.query("SELECT tuskyAccountId, $idName FROM ${entity.simpleName} ORDER BY tuskyAccountId, $idName", null) + cursor.moveToFirst() + while (!cursor.isAfterLast) { + val tuskyAccountId: Long = cursor.getLong(cursor.getColumnIndex("tuskyAccountId")) + val id: String = cursor.getString(cursor.getColumnIndex(idName)) + loadedItems.add(tuskyAccountId to id) + cursor.moveToNext() + } + cursor.close() + + assertEquals(expectedItems, loadedItems) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/db/dao/NotificationsDaoTest.kt b/app/src/test/java/com/keylesspalace/tusky/db/dao/NotificationsDaoTest.kt new file mode 100644 index 0000000..16e6034 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/db/dao/NotificationsDaoTest.kt @@ -0,0 +1,234 @@ +package com.keylesspalace.tusky.db.dao + +import androidx.paging.PagingSource +import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.keylesspalace.tusky.components.notifications.fakeNotification +import com.keylesspalace.tusky.components.notifications.fakeReport +import com.keylesspalace.tusky.components.notifications.insert +import com.keylesspalace.tusky.components.notifications.toNotificationDataEntity +import com.keylesspalace.tusky.components.notifications.toNotificationEntity +import com.keylesspalace.tusky.components.timeline.Placeholder +import com.keylesspalace.tusky.components.timeline.fakeAccount +import com.keylesspalace.tusky.components.timeline.fakeStatus +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.Converters +import com.keylesspalace.tusky.di.NetworkModule +import com.keylesspalace.tusky.entity.Notification +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class NotificationsDaoTest { + private lateinit var notificationsDao: NotificationsDao + private lateinit var db: AppDatabase + + private val moshi = NetworkModule.providesMoshi() + + @Before + fun createDb() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) + .addTypeConverter(Converters(moshi)) + .allowMainThreadQueries() + .build() + notificationsDao = db.notificationsDao() + } + + @After + fun closeDb() { + db.close() + } + + @Test + fun insertAndGetNotification() = runTest { + db.insert( + listOf( + fakeNotification(id = "1"), + fakeNotification(id = "2"), + fakeNotification(id = "3"), + ), + tuskyAccountId = 1 + ) + db.insert( + listOf(fakeNotification(id = "3")), + tuskyAccountId = 2 + ) + + val pagingSource = notificationsDao.getNotifications(tuskyAccountId = 1) + + val loadResult = pagingSource.load(PagingSource.LoadParams.Refresh(null, 2, false)) + + val loadedStatuses = (loadResult as PagingSource.LoadResult.Page).data + + assertEquals( + listOf( + fakeNotification(id = "3").toNotificationDataEntity(1), + fakeNotification(id = "2").toNotificationDataEntity(1) + ), + loadedStatuses + ) + } + + @Test + fun deleteRange() = runTest { + val notifications = listOf( + fakeNotification(id = "100"), + fakeNotification(id = "50"), + fakeNotification(id = "15"), + fakeNotification(id = "14"), + fakeNotification(id = "13"), + fakeNotification(id = "12"), + fakeNotification(id = "11"), + fakeNotification(id = "9") + ) + + db.insert(notifications, 1) + db.insert(listOf(fakeNotification(id = "13")), 2) + + assertEquals(3, notificationsDao.deleteRange(1, "12", "14")) + assertEquals(0, notificationsDao.deleteRange(1, "80", "80")) + assertEquals(0, notificationsDao.deleteRange(1, "60", "80")) + assertEquals(0, notificationsDao.deleteRange(1, "5", "8")) + assertEquals(0, notificationsDao.deleteRange(1, "101", "1000")) + assertEquals(1, notificationsDao.deleteRange(1, "50", "50")) + + val loadParams: PagingSource.LoadParams<Int> = PagingSource.LoadParams.Refresh(null, 100, false) + + val notificationsAccount1 = (notificationsDao.getNotifications(1).load(loadParams) as PagingSource.LoadResult.Page).data + val notificationsAccount2 = (notificationsDao.getNotifications(2).load(loadParams) as PagingSource.LoadResult.Page).data + + val remainingNotificationsAccount1 = listOf( + fakeNotification(id = "100").toNotificationDataEntity(1), + fakeNotification(id = "15").toNotificationDataEntity(1), + fakeNotification(id = "11").toNotificationDataEntity(1), + fakeNotification(id = "9").toNotificationDataEntity(1) + ) + + val remainingNotificationsAccount2 = listOf( + fakeNotification(id = "13").toNotificationDataEntity(2) + ) + + assertEquals(remainingNotificationsAccount1, notificationsAccount1) + assertEquals(remainingNotificationsAccount2, notificationsAccount2) + } + + @Test + fun deleteAllForInstance() = runTest { + val redAccount = fakeNotification(id = "500", account = fakeAccount(id = "500", domain = "mastodon.red")) + val blueAccount = fakeNotification(id = "501", account = fakeAccount(id = "501", domain = "mastodon.blue")) + val redStatus = fakeNotification(id = "502", account = fakeAccount(id = "502", domain = "mastodon.example"), status = fakeStatus(id = "502", domain = "mastodon.red", authorServerId = "502a")) + val blueStatus = fakeNotification(id = "503", account = fakeAccount(id = "503", domain = "mastodon.example"), status = fakeStatus(id = "503", domain = "mastodon.blue", authorServerId = "503a")) + + val redStatus2 = fakeNotification(id = "600", account = fakeAccount(id = "600", domain = "mastodon.red")) + + db.insert(listOf(redAccount, blueAccount, redStatus, blueStatus), 1) + db.insert(listOf(redStatus2), 2) + + notificationsDao.deleteAllFromInstance(1, "mastodon.red") + notificationsDao.deleteAllFromInstance(1, "mastodon.blu") // shouldn't delete anything + notificationsDao.deleteAllFromInstance(1, "mastodon.green") // shouldn't delete anything + + val loadParams: PagingSource.LoadParams<Int> = PagingSource.LoadParams.Refresh(null, 100, false) + + val notificationsAccount1 = (notificationsDao.getNotifications(1).load(loadParams) as PagingSource.LoadResult.Page).data + val notificationsAccount2 = (notificationsDao.getNotifications(2).load(loadParams) as PagingSource.LoadResult.Page).data + + assertEquals( + listOf( + blueStatus.toNotificationDataEntity(1), + blueAccount.toNotificationDataEntity(1) + ), + notificationsAccount1 + ) + assertEquals(listOf(redStatus2.toNotificationDataEntity(2)), notificationsAccount2) + } + + @Test + fun `should return null as topId when db is empty`() = runTest { + assertNull(notificationsDao.getTopId(1)) + } + + @Test + fun `should return correct topId`() = runTest { + db.insert( + listOf( + fakeNotification(id = "100"), + fakeNotification(id = "3"), + fakeNotification(id = "33"), + fakeNotification(id = "8"), + ), + tuskyAccountId = 1 + ) + db.insert( + listOf( + fakeNotification(id = "200"), + fakeNotification(id = "300"), + fakeNotification(id = "1000"), + ), + tuskyAccountId = 2 + ) + + assertEquals("100", notificationsDao.getTopId(1)) + assertEquals("1000", notificationsDao.getTopId(2)) + } + + @Test + fun `should return correct top placeholderId`() = runTest { + val notifications = listOf( + fakeNotification(id = "1000"), + fakeNotification(id = "97"), + fakeNotification(id = "90"), + fakeNotification(id = "77") + ) + db.insert(notifications) + + notificationsDao.insertNotification(Placeholder(id = "99", loading = false).toNotificationEntity(1)) + notificationsDao.insertNotification(Placeholder(id = "96", loading = false).toNotificationEntity(1)) + notificationsDao.insertNotification(Placeholder(id = "80", loading = false).toNotificationEntity(1)) + + assertEquals("99", notificationsDao.getTopPlaceholderId(1)) + } + + @Test + fun `should correctly delete all by user`() = runTest { + val notificationsAccount1 = listOf( + // will be removed because it is a like by account 1 + fakeNotification(id = "1", account = fakeAccount(id = "1"), status = fakeStatus(id = "1", authorServerId = "100")), + // will be removed because it references a status by account 1 + fakeNotification(id = "2", account = fakeAccount(id = "2"), status = fakeStatus(id = "2", authorServerId = "1")), + // will not be removed because they are admin notifications + fakeNotification(type = Notification.Type.REPORT, id = "3", account = fakeAccount(id = "3"), status = null, report = fakeReport(id = "1", targetAccount = fakeAccount(id = "1"))), + fakeNotification(type = Notification.Type.SIGN_UP, id = "4", account = fakeAccount(id = "1"), status = null, report = fakeReport(id = "1", targetAccount = fakeAccount(id = "4"))), + // will not be removed because it does not reference account 1 + fakeNotification(id = "5", account = fakeAccount(id = "5"), status = fakeStatus(id = "5", authorServerId = "100")), + ) + + db.insert(notificationsAccount1, tuskyAccountId = 1) + db.insert(listOf(fakeNotification(id = "6")), tuskyAccountId = 2) + + notificationsDao.removeAllByUser(1, "1") + + val loadedNotifications: MutableList<String> = mutableListOf() + val cursor = db.query("SELECT id FROM NotificationEntity ORDER BY id ASC", null) + cursor.moveToFirst() + while (!cursor.isAfterLast) { + val id: String = cursor.getString(cursor.getColumnIndex("id")) + loadedNotifications.add(id) + cursor.moveToNext() + } + cursor.close() + + val expectedNotifications = listOf("3", "4", "5", "6") + + assertEquals(expectedNotifications, loadedNotifications) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/db/dao/TimelineDaoTest.kt b/app/src/test/java/com/keylesspalace/tusky/db/dao/TimelineDaoTest.kt new file mode 100644 index 0000000..d015c80 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/db/dao/TimelineDaoTest.kt @@ -0,0 +1,340 @@ +package com.keylesspalace.tusky.db.dao + +import androidx.paging.PagingSource +import androidx.room.Room +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.keylesspalace.tusky.components.timeline.fakeHomeTimelineData +import com.keylesspalace.tusky.components.timeline.fakePlaceholderHomeTimelineData +import com.keylesspalace.tusky.components.timeline.insert +import com.keylesspalace.tusky.db.AppDatabase +import com.keylesspalace.tusky.db.Converters +import com.keylesspalace.tusky.di.NetworkModule +import kotlinx.coroutines.test.runTest +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class TimelineDaoTest { + private lateinit var timelineDao: TimelineDao + private lateinit var db: AppDatabase + + private val moshi = NetworkModule.providesMoshi() + + @Before + fun createDb() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + db = Room.inMemoryDatabaseBuilder(context, AppDatabase::class.java) + .addTypeConverter(Converters(moshi)) + .allowMainThreadQueries() + .build() + timelineDao = db.timelineDao() + } + + @After + fun closeDb() { + db.close() + } + + @Test + fun insertGetStatus() = runTest { + val setOne = fakeHomeTimelineData(id = "3") + val setTwo = fakeHomeTimelineData(id = "20", reblogAuthorServerId = "R1") + val ignoredOne = fakeHomeTimelineData(id = "1") + val ignoredTwo = fakeHomeTimelineData(id = "2", tuskyAccountId = 2) + + db.insert( + listOf(setOne, setTwo, ignoredOne), + tuskyAccountId = 1 + ) + db.insert( + listOf(ignoredTwo), + tuskyAccountId = 2 + ) + + val pagingSource = timelineDao.getHomeTimeline(1) + + val loadResult = pagingSource.load(PagingSource.LoadParams.Refresh(null, 2, false)) + + val loadedStatuses = (loadResult as PagingSource.LoadResult.Page).data + + assertEquals(2, loadedStatuses.size) + assertEquals(listOf(setTwo, setOne), loadedStatuses) + } + + @Test + fun overwriteDeletedStatus() = runTest { + val oldStatuses = listOf( + fakeHomeTimelineData(id = "3"), + fakeHomeTimelineData(id = "2"), + fakeHomeTimelineData(id = "1") + ) + + db.insert(oldStatuses, 1) + + // status 2 gets deleted, newly loaded status contain only 1 + 3 + val newStatuses = listOf( + fakeHomeTimelineData(id = "3"), + fakeHomeTimelineData(id = "1") + ) + + val deletedCount = timelineDao.deleteRange(1, newStatuses.last().id, newStatuses.first().id) + assertEquals(3, deletedCount) + + db.insert(newStatuses, 1) + + // make sure status 2 is no longer in db + val pagingSource = timelineDao.getHomeTimeline(1) + + val loadResult = pagingSource.load(PagingSource.LoadParams.Refresh(null, 100, false)) + + val loadedStatuses = (loadResult as PagingSource.LoadResult.Page).data + + assertEquals(newStatuses, loadedStatuses) + } + + @Test + fun deleteRange() = runTest { + val statuses = listOf( + fakeHomeTimelineData(id = "100"), + fakeHomeTimelineData(id = "50"), + fakeHomeTimelineData(id = "15"), + fakeHomeTimelineData(id = "14"), + fakeHomeTimelineData(id = "13"), + fakeHomeTimelineData(id = "13", tuskyAccountId = 2), + fakeHomeTimelineData(id = "12"), + fakeHomeTimelineData(id = "11"), + fakeHomeTimelineData(id = "9") + ) + + db.insert(statuses - statuses[5], 1) + db.insert(listOf(statuses[5]), 2) + + assertEquals(3, timelineDao.deleteRange(1, "12", "14")) + assertEquals(0, timelineDao.deleteRange(1, "80", "80")) + assertEquals(0, timelineDao.deleteRange(1, "60", "80")) + assertEquals(0, timelineDao.deleteRange(1, "5", "8")) + assertEquals(0, timelineDao.deleteRange(1, "101", "1000")) + assertEquals(1, timelineDao.deleteRange(1, "50", "50")) + + val loadParams: PagingSource.LoadParams<Int> = PagingSource.LoadParams.Refresh(null, 100, false) + + val statusesAccount1 = (timelineDao.getHomeTimeline(1).load(loadParams) as PagingSource.LoadResult.Page).data + val statusesAccount2 = (timelineDao.getHomeTimeline(2).load(loadParams) as PagingSource.LoadResult.Page).data + + val remainingStatusesAccount1 = listOf( + fakeHomeTimelineData(id = "100"), + fakeHomeTimelineData(id = "15"), + fakeHomeTimelineData(id = "11"), + fakeHomeTimelineData(id = "9") + ) + + val remainingStatusesAccount2 = listOf( + fakeHomeTimelineData(id = "13", tuskyAccountId = 2) + ) + + assertEquals(remainingStatusesAccount1, statusesAccount1) + assertEquals(remainingStatusesAccount2, statusesAccount2) + } + + @Test + fun deleteAllForInstance() = runTest { + val statusWithRedDomain1 = fakeHomeTimelineData( + id = "15", + tuskyAccountId = 1, + domain = "mastodon.red", + authorServerId = "1" + ) + val statusWithRedDomain2 = fakeHomeTimelineData( + id = "14", + tuskyAccountId = 1, + domain = "mastodon.red", + authorServerId = "2" + ) + val statusWithRedDomainOtherAccount = fakeHomeTimelineData( + id = "12", + tuskyAccountId = 2, + domain = "mastodon.red", + authorServerId = "2" + ) + val statusWithBlueDomain = fakeHomeTimelineData( + id = "10", + tuskyAccountId = 1, + domain = "mastodon.blue", + authorServerId = "4" + ) + val statusWithBlueDomainOtherAccount = fakeHomeTimelineData( + id = "10", + tuskyAccountId = 2, + domain = "mastodon.blue", + authorServerId = "5" + ) + val statusWithGreenDomain = fakeHomeTimelineData( + id = "8", + tuskyAccountId = 1, + domain = "mastodon.green", + authorServerId = "6" + ) + + db.insert(listOf(statusWithRedDomain1, statusWithRedDomain2, statusWithBlueDomain, statusWithGreenDomain), 1) + db.insert(listOf(statusWithRedDomainOtherAccount, statusWithBlueDomainOtherAccount), 2) + + timelineDao.deleteAllFromInstance(1, "mastodon.red") + timelineDao.deleteAllFromInstance(1, "mastodon.blu") // shouldn't delete anything + timelineDao.deleteAllFromInstance(1, "greenmastodon.green") // shouldn't delete anything + + val loadParams: PagingSource.LoadParams<Int> = PagingSource.LoadParams.Refresh(null, 100, false) + + val statusesAccount1 = (timelineDao.getHomeTimeline(1).load(loadParams) as PagingSource.LoadResult.Page).data + val statusesAccount2 = (timelineDao.getHomeTimeline(2).load(loadParams) as PagingSource.LoadResult.Page).data + + assertEquals(listOf(statusWithBlueDomain, statusWithGreenDomain), statusesAccount1) + assertEquals(listOf(statusWithRedDomainOtherAccount, statusWithBlueDomainOtherAccount), statusesAccount2) + } + + @Test + fun `should return null as topId when db is empty`() = runTest { + assertNull(timelineDao.getTopId(1)) + } + + @Test + fun `should return correct topId`() = runTest { + val statusData = listOf( + fakeHomeTimelineData( + id = "4", + tuskyAccountId = 1, + domain = "mastodon.test", + authorServerId = "1" + ), + fakeHomeTimelineData( + id = "33", + tuskyAccountId = 1, + domain = "mastodon.test", + authorServerId = "2" + ), + fakeHomeTimelineData( + id = "22", + tuskyAccountId = 1, + domain = "mastodon.test", + authorServerId = "2" + ) + ) + + db.insert(statusData, 1) + + assertEquals("33", timelineDao.getTopId(1)) + } + + @Test + fun `should return correct top placeholderId`() = runTest { + val statusData = listOf( + fakeHomeTimelineData(id = "1000"), + fakePlaceholderHomeTimelineData(id = "99"), + fakeHomeTimelineData(id = "97"), + fakePlaceholderHomeTimelineData(id = "96"), + fakeHomeTimelineData(id = "90"), + fakePlaceholderHomeTimelineData(id = "80"), + fakeHomeTimelineData(id = "77") + ) + + db.insert(statusData) + + assertEquals("99", timelineDao.getTopPlaceholderId(1)) + } + + @Test + fun `should correctly delete all by user`() = runTest { + val statusData = listOf( + // will be deleted because it is a direct post + fakeHomeTimelineData(id = "0", tuskyAccountId = 1, authorServerId = "1"), + // different Tusky Account + fakeHomeTimelineData(id = "1", tuskyAccountId = 2, authorServerId = "1"), + // different author + fakeHomeTimelineData(id = "2", tuskyAccountId = 1, authorServerId = "2"), + // different author and reblogger + fakeHomeTimelineData(id = "3", tuskyAccountId = 1, authorServerId = "2", statusId = "100", reblogAuthorServerId = "3"), + // will be deleted because it is a reblog + fakeHomeTimelineData(id = "4", tuskyAccountId = 1, authorServerId = "2", statusId = "101", reblogAuthorServerId = "1"), + // not a status + fakePlaceholderHomeTimelineData(id = "5"), + // will be deleted because it is a self reblog + fakeHomeTimelineData(id = "6", tuskyAccountId = 1, authorServerId = "1", statusId = "102", reblogAuthorServerId = "1"), + // will be deleted because it direct post reblogged by another user + fakeHomeTimelineData(id = "7", tuskyAccountId = 1, authorServerId = "1", statusId = "103", reblogAuthorServerId = "3"), + // different Tusky Account + fakeHomeTimelineData(id = "8", tuskyAccountId = 2, authorServerId = "3", statusId = "104", reblogAuthorServerId = "2"), + // different Tusky Account + fakeHomeTimelineData(id = "9", tuskyAccountId = 2, authorServerId = "3", statusId = "105", reblogAuthorServerId = "1"), + ) + + db.insert(statusData - statusData[1] - statusData[8] - statusData [9], tuskyAccountId = 1) + db.insert(listOf(statusData[1], statusData[8], statusData [9]), tuskyAccountId = 2) + + timelineDao.removeAllByUser(1, "1") + + val loadedHomeTimelineItems: MutableList<String> = mutableListOf() + val accountCursor = db.query("SELECT id FROM HomeTimelineEntity ORDER BY id ASC", null) + accountCursor.moveToFirst() + while (!accountCursor.isAfterLast) { + val id: String = accountCursor.getString(accountCursor.getColumnIndex("id")) + loadedHomeTimelineItems.add(id) + accountCursor.moveToNext() + } + accountCursor.close() + + val expectedHomeTimelineItems = listOf("1", "2", "3", "5", "8", "9") + + assertEquals(expectedHomeTimelineItems, loadedHomeTimelineItems) + } + + @Test + fun `should correctly delete statuses and reblogs by user`() = runTest { + val statusData = listOf( + // will be deleted because it is a direct post + fakeHomeTimelineData(id = "0", tuskyAccountId = 1, authorServerId = "1"), + // different Tusky Account + fakeHomeTimelineData(id = "1", tuskyAccountId = 2, authorServerId = "1"), + // different author + fakeHomeTimelineData(id = "2", tuskyAccountId = 1, authorServerId = "2"), + // different author and reblogger + fakeHomeTimelineData(id = "3", tuskyAccountId = 1, authorServerId = "2", statusId = "100", reblogAuthorServerId = "3"), + // will be deleted because it is a reblog + fakeHomeTimelineData(id = "4", tuskyAccountId = 1, authorServerId = "2", statusId = "101", reblogAuthorServerId = "1"), + // not a status + fakePlaceholderHomeTimelineData(id = "5"), + // will be deleted because it is a self reblog + fakeHomeTimelineData(id = "6", tuskyAccountId = 1, authorServerId = "1", statusId = "102", reblogAuthorServerId = "1"), + // will NOT be deleted because it direct post reblogged by another user + fakeHomeTimelineData(id = "7", tuskyAccountId = 1, authorServerId = "1", statusId = "103", reblogAuthorServerId = "3"), + // different Tusky Account + fakeHomeTimelineData(id = "8", tuskyAccountId = 2, authorServerId = "3", statusId = "104", reblogAuthorServerId = "2"), + // different Tusky Account + fakeHomeTimelineData(id = "9", tuskyAccountId = 2, authorServerId = "3", statusId = "105", reblogAuthorServerId = "1"), + ) + + db.insert(statusData - statusData[1] - statusData[8] - statusData [9], tuskyAccountId = 1) + db.insert(listOf(statusData[1], statusData[8], statusData [9]), tuskyAccountId = 2) + + timelineDao.removeStatusesAndReblogsByUser(1, "1") + + val loadedHomeTimelineItems: MutableList<String> = mutableListOf() + val accountCursor = db.query("SELECT id FROM HomeTimelineEntity ORDER BY id ASC", null) + accountCursor.moveToFirst() + while (!accountCursor.isAfterLast) { + val id: String = accountCursor.getString(accountCursor.getColumnIndex("id")) + loadedHomeTimelineItems.add(id) + accountCursor.moveToNext() + } + accountCursor.close() + + val expectedHomeTimelineItems = listOf("1", "2", "3", "5", "7", "8", "9") + + assertEquals(expectedHomeTimelineItems, loadedHomeTimelineItems) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/entity/ProxyConfigurationTest.kt b/app/src/test/java/com/keylesspalace/tusky/entity/ProxyConfigurationTest.kt new file mode 100644 index 0000000..1f7bbed --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/entity/ProxyConfigurationTest.kt @@ -0,0 +1,46 @@ +package com.keylesspalace.tusky.entity + +import com.keylesspalace.tusky.settings.ProxyConfiguration +import org.junit.Assert +import org.junit.Test + +class ProxyConfigurationTest { + @Test + fun `serialized non-int is not valid proxy port`() { + Assert.assertFalse(ProxyConfiguration.isValidProxyPort("should fail")) + Assert.assertFalse(ProxyConfiguration.isValidProxyPort("1.5")) + } + + @Test + fun `number outside port range is not valid`() { + Assert.assertFalse(ProxyConfiguration.isValidProxyPort("${ProxyConfiguration.MIN_PROXY_PORT - 1}")) + Assert.assertFalse(ProxyConfiguration.isValidProxyPort("${ProxyConfiguration.MAX_PROXY_PORT + 1}")) + } + + @Test + fun `number in port range, inclusive of min and max, is valid`() { + Assert.assertTrue(ProxyConfiguration.isValidProxyPort(ProxyConfiguration.MIN_PROXY_PORT)) + Assert.assertTrue(ProxyConfiguration.isValidProxyPort(ProxyConfiguration.MAX_PROXY_PORT)) + Assert.assertTrue(ProxyConfiguration.isValidProxyPort((ProxyConfiguration.MIN_PROXY_PORT + ProxyConfiguration.MAX_PROXY_PORT) / 2)) + } + + @Test + fun `create with invalid port yields null`() { + Assert.assertNull(ProxyConfiguration.create("hostname", ProxyConfiguration.MIN_PROXY_PORT - 1)) + } + + @Test + fun `create with invalid hostname yields null`() { + Assert.assertNull(ProxyConfiguration.create(".", ProxyConfiguration.MIN_PROXY_PORT)) + } + + @Test + fun `create with valid hostname and port yields the config object`() { + Assert.assertTrue(ProxyConfiguration.create("hostname", ProxyConfiguration.MIN_PROXY_PORT) is ProxyConfiguration) + } + + @Test + fun `unicode hostname allowed`() { + Assert.assertTrue(ProxyConfiguration.create("federação.social", ProxyConfiguration.MIN_PROXY_PORT) is ProxyConfiguration) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/json/GuardedAdapterTest.kt b/app/src/test/java/com/keylesspalace/tusky/json/GuardedAdapterTest.kt new file mode 100644 index 0000000..87ae8e5 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/json/GuardedAdapterTest.kt @@ -0,0 +1,134 @@ +package com.keylesspalace.tusky.json + +import com.keylesspalace.tusky.entity.Relationship +import com.squareup.moshi.Moshi +import com.squareup.moshi.adapter +import org.junit.Assert.assertEquals +import org.junit.Test + +@OptIn(ExperimentalStdlibApi::class) +class GuardedAdapterTest { + + private val moshi = Moshi.Builder() + .add(GuardedAdapter.ANNOTATION_FACTORY) + .build() + + @Test + fun `should deserialize Relationship when attribute 'subscribing' is a boolean`() { + val jsonInput = """ + { + "id": "1", + "following": true, + "showing_reblogs": true, + "notifying": false, + "followed_by": true, + "blocking": false, + "blocked_by": false, + "muting": false, + "muting_notifications": false, + "requested": false, + "domain_blocking": false, + "endorsed": false, + "note": "Hi", + "subscribing": true + } + """.trimIndent() + + assertEquals( + Relationship( + id = "1", + following = true, + followedBy = true, + blocking = false, + muting = false, + mutingNotifications = false, + requested = false, + showingReblogs = true, + subscribing = true, + blockingDomain = false, + note = "Hi", + notifying = false + ), + moshi.adapter<Relationship>().fromJson(jsonInput) + ) + } + + @Test + fun `should deserialize Relationship when attribute 'subscribing' is an object`() { + val jsonInput = """ + { + "id": "2", + "following": true, + "showing_reblogs": true, + "notifying": false, + "followed_by": true, + "blocking": false, + "blocked_by": false, + "muting": false, + "muting_notifications": false, + "requested": false, + "domain_blocking": false, + "endorsed": false, + "note": "Hi", + "subscribing": { } + } + """.trimIndent() + + assertEquals( + Relationship( + id = "2", + following = true, + followedBy = true, + blocking = false, + muting = false, + mutingNotifications = false, + requested = false, + showingReblogs = true, + subscribing = null, + blockingDomain = false, + note = "Hi", + notifying = false + ), + moshi.adapter<Relationship>().fromJson(jsonInput) + ) + } + + @Test + fun `should deserialize Relationship when attribute 'subscribing' does not exist`() { + val jsonInput = """ + { + "id": "3", + "following": true, + "showing_reblogs": true, + "notifying": false, + "followed_by": true, + "blocking": false, + "blocked_by": false, + "muting": false, + "muting_notifications": false, + "requested": false, + "domain_blocking": false, + "endorsed": false, + "note": "Hi" + } + """.trimIndent() + + assertEquals( + Relationship( + id = "3", + following = true, + followedBy = true, + blocking = false, + muting = false, + mutingNotifications = false, + requested = false, + showingReblogs = true, + subscribing = null, + blockingDomain = false, + note = "Hi", + notifying = false + ), + moshi.adapter<Relationship>().fromJson(jsonInput) + ) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptorTest.kt b/app/src/test/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptorTest.kt new file mode 100644 index 0000000..206e27d --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/network/InstanceSwitchAuthInterceptorTest.kt @@ -0,0 +1,142 @@ +package com.keylesspalace.tusky.network + +import com.keylesspalace.tusky.db.AccountManager +import com.keylesspalace.tusky.db.entity.AccountEntity +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock + +class InstanceSwitchAuthInterceptorTest { + + private val mockWebServer = MockWebServer() + + @Before + fun setup() { + mockWebServer.start() + } + + @After + fun teardown() { + mockWebServer.shutdown() + } + + @Test + fun `should make regular request when requested`() { + mockWebServer.enqueue(MockResponse()) + + val accountManager: AccountManager = mock { + on { activeAccount } doAnswer { null } + } + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) + .build() + + val request = Request.Builder() + .get() + .url(mockWebServer.url("/test")) + .build() + + val response = okHttpClient.newCall(request).execute() + + assertEquals(200, response.code) + } + + @Test + fun `should make request to instance requested in special header`() { + mockWebServer.enqueue(MockResponse()) + + val accountManager: AccountManager = mock { + on { activeAccount } doAnswer { + AccountEntity( + id = 1, + domain = "test.domain", + accessToken = "fakeToken", + clientId = "fakeId", + clientSecret = "fakeSecret", + isActive = true + ) + } + } + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) + .build() + + val request = Request.Builder() + .get() + .url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + ":" + mockWebServer.port + "/test") + .header(MastodonApi.DOMAIN_HEADER, mockWebServer.hostName) + .build() + + val response = okHttpClient.newCall(request).execute() + + assertEquals(200, response.code) + + assertNull(mockWebServer.takeRequest().getHeader("Authorization")) + } + + @Test + fun `should make request to current instance when requested and user is logged in`() { + mockWebServer.enqueue(MockResponse()) + + val accountManager: AccountManager = mock { + on { activeAccount } doAnswer { + AccountEntity( + id = 1, + domain = mockWebServer.hostName, + accessToken = "fakeToken", + clientId = "fakeId", + clientSecret = "fakeSecret", + isActive = true + ) + } + } + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) + .build() + + val request = Request.Builder() + .get() + .url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + ":" + mockWebServer.port + "/test") + .build() + + val response = okHttpClient.newCall(request).execute() + + assertEquals(200, response.code) + + assertEquals("Bearer fakeToken", mockWebServer.takeRequest().getHeader("Authorization")) + } + + @Test + fun `should fail to make request when request to current instance is requested but no user is logged in`() { + mockWebServer.enqueue(MockResponse()) + + val accountManager: AccountManager = mock { + on { activeAccount } doAnswer { null } + } + + val okHttpClient = OkHttpClient.Builder() + .addInterceptor(InstanceSwitchAuthInterceptor(accountManager)) + .build() + + val request = Request.Builder() + .get() + .url("http://" + MastodonApi.PLACEHOLDER_DOMAIN + "/test") + .build() + + val response = okHttpClient.newCall(request).execute() + + assertEquals(400, response.code) + assertEquals(0, mockWebServer.requestCount) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/usecase/TimelineCasesTest.kt b/app/src/test/java/com/keylesspalace/tusky/usecase/TimelineCasesTest.kt new file mode 100644 index 0000000..7ae23d2 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/usecase/TimelineCasesTest.kt @@ -0,0 +1,110 @@ +package com.keylesspalace.tusky.usecase + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import app.cash.turbine.test +import at.connyduck.calladapter.networkresult.NetworkResult +import com.keylesspalace.tusky.appstore.EventHub +import com.keylesspalace.tusky.appstore.StatusChangedEvent +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.network.MastodonApi +import java.util.Date +import kotlinx.coroutines.runBlocking +import okhttp3.ResponseBody.Companion.toResponseBody +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.stub +import org.robolectric.annotation.Config +import retrofit2.HttpException +import retrofit2.Response + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class TimelineCasesTest { + + private lateinit var api: MastodonApi + private lateinit var eventHub: EventHub + private lateinit var timelineCases: TimelineCases + + private val statusId = "1234" + + @Before + fun setup() { + api = mock() + eventHub = EventHub() + timelineCases = TimelineCases(api, eventHub) + } + + @Test + fun `pin success emits StatusChangedEvent`() { + val pinnedStatus = mockStatus(pinned = true) + + api.stub { + onBlocking { pinStatus(statusId) } doReturn NetworkResult.success(pinnedStatus) + } + + runBlocking { + eventHub.events.test { + timelineCases.pin(statusId, true) + assertEquals(StatusChangedEvent(pinnedStatus), awaitItem()) + } + } + } + + @Test + fun `pin failure with server error throws TimelineError with server message`() { + api.stub { + onBlocking { pinStatus(statusId) } doReturn NetworkResult.failure( + HttpException( + Response.error<Status>( + 422, + "{\"error\":\"Validation Failed: You have already pinned the maximum number of toots\"}".toResponseBody() + ) + ) + ) + } + runBlocking { + assertEquals( + "Validation Failed: You have already pinned the maximum number of toots", + timelineCases.pin(statusId, true).exceptionOrNull()?.message + ) + } + } + + private fun mockStatus(pinned: Boolean = false): Status { + return Status( + id = "123", + url = "https://mastodon.social/@Tusky/100571663297225812", + account = mock(), + inReplyToId = null, + inReplyToAccountId = null, + reblog = null, + content = "", + createdAt = Date(), + editedAt = null, + emojis = emptyList(), + reblogsCount = 0, + favouritesCount = 0, + repliesCount = 0, + reblogged = false, + favourited = false, + bookmarked = false, + sensitive = false, + spoilerText = "", + visibility = Status.Visibility.PUBLIC, + attachments = arrayListOf(), + mentions = listOf(), + tags = listOf(), + application = null, + pinned = pinned, + muted = false, + poll = null, + card = null, + language = null, + filtered = emptyList() + ) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatterTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatterTest.kt new file mode 100644 index 0000000..3fc2ee4 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/util/AbsoluteTimeFormatterTest.kt @@ -0,0 +1,69 @@ +package com.keylesspalace.tusky.util + +import java.time.Instant +import java.util.* +import org.junit.AfterClass +import org.junit.Assert.assertEquals +import org.junit.BeforeClass +import org.junit.Test + +class AbsoluteTimeFormatterTest { + companion object { + /** Default locale before this test started */ + private lateinit var locale: Locale + + /** + * Ensure the Locale is ENGLISH so that tests against literal strings like + * "Apr" later, even if the test host's locale is e.g. FRENCH which would + * normally report "avr.". + */ + @BeforeClass + @JvmStatic + fun beforeClass() { + locale = Locale.getDefault() + Locale.setDefault(Locale.ENGLISH) + } + + @AfterClass + @JvmStatic + fun afterClass() { + Locale.setDefault(locale) + } + } + + private val formatter = AbsoluteTimeFormatter(TimeZone.getTimeZone("UTC")) + private val now = Date.from(Instant.parse("2022-04-11T00:00:00.00Z")) + + @Test + fun `null handling`() { + assertEquals("??", formatter.format(null, true, now)) + assertEquals("??", formatter.format(null, false, now)) + } + + @Test + fun `same day formatting`() { + val tenTen = Date.from(Instant.parse("2022-04-11T10:10:00.00Z")) + assertEquals("10:10", formatter.format(tenTen, true, now)) + assertEquals("10:10", formatter.format(tenTen, false, now)) + } + + @Test + fun `same year formatting`() { + val nextDay = Date.from(Instant.parse("2022-04-12T00:10:00.00Z")) + assertEquals("12 Apr, 00:10", formatter.format(nextDay, true, now)) + assertEquals("12 Apr, 00:10", formatter.format(nextDay, false, now)) + val endOfYear = Date.from(Instant.parse("2022-12-31T23:59:00.00Z")) + assertEquals("31 Dec, 23:59", formatter.format(endOfYear, true, now)) + assertEquals("31 Dec, 23:59", formatter.format(endOfYear, false, now)) + } + + @Test + fun `other year formatting`() { + val firstDayNextYear = Date.from(Instant.parse("2023-01-01T00:00:00.00Z")) + assertEquals("2023-01-01", formatter.format(firstDayNextYear, true, now)) + assertEquals("2023-01-01 00:00", formatter.format(firstDayNextYear, false, now)) + val inTenYears = Date.from(Instant.parse("2032-04-11T10:10:00.00Z")) + assertEquals("2032-04-11", formatter.format(inTenYears, true, now)) + assertEquals("2032-04-11 10:10", formatter.format(inTenYears, false, now)) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/util/HttpHeaderLinkTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/HttpHeaderLinkTest.kt new file mode 100644 index 0000000..ac253db --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/util/HttpHeaderLinkTest.kt @@ -0,0 +1,77 @@ +package com.keylesspalace.tusky.util + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class HttpHeaderLinkTest { + data class TestData(val name: String, val input: String, val want: List<HttpHeaderLink>) + + @Test + fun shouldParseValidLinks() { + val testData = arrayOf( + // Examples from https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link + TestData( + "Single URL", + "<https://example.com>", + listOf(HttpHeaderLink("https://example.com")) + ), + TestData( + "Single URL with parameters", + "<https://example.com>; rel=\"preconnect\"", + listOf(HttpHeaderLink("https://example.com")) + ), + TestData( + "Single encoded URL with parameters", + "<https://example.com/%E8%8B%97%E6%9D%A1>; rel=\"preconnect\"", + listOf(HttpHeaderLink("https://example.com/%E8%8B%97%E6%9D%A1")) + ), + TestData( + "Multiple URLs, separated by commas", + "<https://one.example.com>; rel=\"preconnect\", <https://two.example.com>; rel=\"preconnect\", <https://three.example.com>; rel=\"preconnect\"", + listOf( + HttpHeaderLink("https://one.example.com"), + HttpHeaderLink("https://two.example.com"), + HttpHeaderLink("https://three.example.com") + ) + ), + // Examples from https://httpwg.org/specs/rfc8288.html#rfc.section.3.5 + TestData( + "Single URL, multiple parameters", + "<http://example.com/TheBook/chapter2>; rel=\"previous\"; title=\"previous chapter\"", + listOf(HttpHeaderLink("http://example.com/TheBook/chapter2")) + ), + TestData( + "Root resource", + "</>; rel=\"http://example.net/foo\"", + listOf(HttpHeaderLink("/")) + ), + TestData( + "Terms and anchor", + "</terms>; rel=\"copyright\"; anchor=\"#foo\"", + listOf(HttpHeaderLink("/terms")) + ), + TestData( + "Multiple URLs with parameter encoding", + "</TheBook/chapter2>; rel=\"previous\"; title*=UTF-8'de'letztes%20Kapitel, </TheBook/chapter4>; rel=\"next\"; title*=UTF-8'de'n%c3%a4chstes%20Kapitel", + listOf( + HttpHeaderLink("/TheBook/chapter2"), + HttpHeaderLink("/TheBook/chapter4") + ) + ) + ) + + // Verify that the URLs are parsed correctly + for (test in testData) { + val links = HttpHeaderLink.parse(test.input) + assertEquals("${test.name}: Same size", links.size, test.want.size) + for (i in links.indices) { + assertEquals(test.name, test.want[i].uri, links[i].uri) + } + } + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/util/LinkHelperTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/LinkHelperTest.kt new file mode 100644 index 0000000..99be370 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/util/LinkHelperTest.kt @@ -0,0 +1,381 @@ +package com.keylesspalace.tusky.util + +import android.text.SpannableStringBuilder +import android.text.style.URLSpan +import android.widget.TextView +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.keylesspalace.tusky.R +import com.keylesspalace.tusky.entity.HashTag +import com.keylesspalace.tusky.entity.Status +import com.keylesspalace.tusky.interfaces.LinkListener +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.robolectric.annotation.Config + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class LinkHelperTest { + private val listener = object : LinkListener { + override fun onViewTag(tag: String) { } + override fun onViewAccount(id: String) { } + override fun onViewUrl(url: String) { } + } + + private val mentions = listOf( + Status.Mention("1", "https://example.com/@user", "user", "user"), + Status.Mention("2", "https://example.com/@anotherUser", "anotherUser", "anotherUser") + ) + private val tags = listOf( + HashTag("Tusky", "https://example.com/Tags/Tusky"), + HashTag("mastodev", "https://example.com/Tags/mastodev") + ) + + private val textView: TextView + get() = TextView(InstrumentationRegistry.getInstrumentation().targetContext) + + @Test + fun whenSettingClickableText_mentionUrlsArePreserved() { + val builder = SpannableStringBuilder() + for (mention in mentions) { + builder.append("@${mention.username}", URLSpan(mention.url), 0) + builder.append(" ") + } + + var urlSpans = builder.getSpans(0, builder.length, URLSpan::class.java) + for (span in urlSpans) { + setClickableText(span, builder, mentions, null, listener) + } + + urlSpans = builder.getSpans(0, builder.length, URLSpan::class.java) + for (span in urlSpans) { + Assert.assertNotNull(mentions.firstOrNull { it.url == span.url }) + } + } + + @Test + fun whenSettingClickableText_nonMentionsAreNotConvertedToMentions() { + val builder = SpannableStringBuilder() + val nonMentionUrl = "http://example.com/" + for (mention in mentions) { + builder.append("@${mention.username}", URLSpan(nonMentionUrl), 0) + builder.append(" ") + builder.append("@${mention.username} ") + } + + var urlSpans = builder.getSpans(0, builder.length, URLSpan::class.java) + for (span in urlSpans) { + setClickableText(span, builder, mentions, null, listener) + } + + urlSpans = builder.getSpans(0, builder.length, URLSpan::class.java) + for (span in urlSpans) { + Assert.assertEquals(nonMentionUrl, span.url) + } + } + + @Test + fun whenCheckingTags_tagNameIsComparedCaseInsensitively() { + for (tag in tags) { + for (mutatedTagName in listOf(tag.name, tag.name.uppercase(), tag.name.lowercase())) { + val tagName = getTagName("#$mutatedTagName", tags) + Assert.assertNotNull(tagName) + Assert.assertNotNull(tags.firstOrNull { it.name == tagName }) + } + } + } + + @Test + fun whenCheckingTags_tagNameIsNormalized() { + val mutator = "aeiou".toList().zip("åÉîøÜ".toList()).toMap() + for (tag in tags) { + val mutatedTagName = String(tag.name.map { mutator[it] ?: it }.toCharArray()) + val tagName = getTagName("#$mutatedTagName", tags) + Assert.assertNotNull(tagName) + Assert.assertNotNull(tags.firstOrNull { it.name == tagName }) + } + } + + @Test + fun hashedUrlSpans_withNoMatchingTag_areNotModified() { + for (tag in tags) { + Assert.assertNull(getTagName("#not${tag.name}", tags)) + } + } + + @Test + fun whenTagsAreNull_tagNameIsGeneratedFromText() { + for (tag in tags) { + Assert.assertEquals(tag.name, getTagName("#${tag.name}", null)) + } + } + + @Test + fun whenStringIsInvalidUri_emptyStringIsReturnedFromGetDomain() { + listOf( + null, + "foo bar baz", + "http:/foo.bar", + "c:/foo/bar" + ).forEach { + Assert.assertEquals("", getDomain(it)) + } + } + + @Test + fun whenUrlIsValid_correctDomainIsReturned() { + listOf( + "example.com", + "localhost", + "sub.domain.com", + "10.45.0.123" + ).forEach { domain -> + listOf( + "https://$domain", + "https://$domain/", + "https://$domain/foo/bar", + "https://$domain/foo/bar.html", + "https://$domain/foo/bar.html#", + "https://$domain/foo/bar.html#anchor", + "https://$domain/foo/bar.html?argument=value", + "https://$domain/foo/bar.html?argument=value&otherArgument=otherValue" + ).forEach { url -> + Assert.assertEquals(domain, getDomain(url)) + } + } + } + + @Test + fun wwwPrefixIsStrippedFromGetDomain() { + mapOf( + "https://www.example.com/foo/bar" to "example.com", + "https://awww.example.com/foo/bar" to "awww.example.com", + "http://www.localhost" to "localhost", + "https://wwwexample.com/" to "wwwexample.com" + ).forEach { (url, domain) -> + Assert.assertEquals(domain, getDomain(url)) + } + } + + @Test + fun hiddenDomainsAreMarkedUp() { + val displayedContent = "This is a good place to go" + val maliciousDomain = "malicious.place" + val maliciousUrl = "https://$maliciousDomain/to/go" + val content = SpannableStringBuilder() + content.append(displayedContent, URLSpan(maliciousUrl), 0) + val oldContent = content.toString() + Assert.assertEquals( + textView.context.getString(R.string.url_domain_notifier, displayedContent, maliciousDomain), + markupHiddenUrls(textView, content).toString() + ) + Assert.assertEquals(oldContent, content.toString()) + } + + @Test + fun fraudulentDomainsAreMarkedUp() { + val displayedContent = "https://tusky.app/" + val maliciousDomain = "malicious.place" + val maliciousUrl = "https://$maliciousDomain/to/go" + val content = SpannableStringBuilder() + content.append(displayedContent, URLSpan(maliciousUrl), 0) + Assert.assertEquals( + textView.context.getString(R.string.url_domain_notifier, displayedContent, maliciousDomain), + markupHiddenUrls(textView, content).toString() + ) + } + + @Test + fun multipleHiddenDomainsAreMarkedUp() { + val domains = listOf("one.place", "another.place", "athird.place") + val displayedContent = "link" + val content = SpannableStringBuilder() + for (domain in domains) { + content.append(displayedContent, URLSpan("https://$domain/foo/bar"), 0) + } + + val markedUpContent = markupHiddenUrls(textView, content) + for (domain in domains) { + Assert.assertTrue(markedUpContent.contains(textView.context.getString(R.string.url_domain_notifier, displayedContent, domain))) + } + } + + @Test + fun nonUriTextExactlyMatchingDomainIsNotMarkedUp() { + val domain = "some.place" + val content = SpannableStringBuilder() + .append(domain, URLSpan("https://$domain/"), 0) + .append(domain, URLSpan("https://$domain"), 0) + .append(domain, URLSpan("https://www.$domain"), 0) + .append("www.$domain", URLSpan("https://$domain"), 0) + .append("www.$domain", URLSpan("https://$domain/"), 0) + .append("$domain/", URLSpan("https://$domain/"), 0) + .append("$domain/", URLSpan("https://$domain"), 0) + .append("$domain/", URLSpan("https://www.$domain"), 0) + + val markedUpContent = markupHiddenUrls(textView, content) + Assert.assertFalse(markedUpContent.contains("🔗")) + } + + @Test + fun spanEndsWithUrlIsNotMarkedUp() { + val content = SpannableStringBuilder() + .append("Some Place: some.place", URLSpan("https://some.place"), 0) + .append("Some Place: some.place/", URLSpan("https://some.place/"), 0) + .append("Some Place - https://some.place", URLSpan("https://some.place"), 0) + .append("Some Place | https://some.place/", URLSpan("https://some.place/"), 0) + .append("Some Place https://some.place/path", URLSpan("https://some.place/path"), 0) + + val markedUpContent = markupHiddenUrls(textView, content) + Assert.assertFalse(markedUpContent.contains("🔗")) + } + + @Test + fun spanEndsWithFraudulentUrlIsMarkedUp() { + val content = SpannableStringBuilder() + .append("Another Place: another.place", URLSpan("https://some.place"), 0) + .append("Another Place: another.place/", URLSpan("https://some.place/"), 0) + .append("Another Place - https://another.place", URLSpan("https://some.place"), 0) + .append("Another Place | https://another.place/", URLSpan("https://some.place/"), 0) + .append("Another Place https://another.place/path", URLSpan("https://some.place/path"), 0) + + val markedUpContent = markupHiddenUrls(textView, content) + val asserts = listOf( + "Another Place: another.place", + "Another Place: another.place/", + "Another Place - https://another.place", + "Another Place | https://another.place/", + "Another Place https://another.place/path" + ) + asserts.forEach { + Assert.assertTrue(markedUpContent.contains(textView.context.getString(R.string.url_domain_notifier, it, "some.place"))) + } + } + + @Test + fun validMentionsAreNotMarkedUp() { + val builder = SpannableStringBuilder() + for (mention in mentions) { + builder.append("@${mention.username}", URLSpan(mention.url), 0) + builder.append(" ") + } + + val markedUpContent = markupHiddenUrls(textView, builder) + for (mention in mentions) { + Assert.assertFalse(markedUpContent.contains("${getDomain(mention.url)})")) + } + } + + @Test + fun invalidMentionsAreNotMarkedUp() { + val builder = SpannableStringBuilder() + for (mention in mentions) { + builder.append("@${mention.username}", URLSpan(mention.url), 0) + builder.append(" ") + } + + val markedUpContent = markupHiddenUrls(textView, builder) + for (mention in mentions) { + Assert.assertFalse(markedUpContent.contains("${getDomain(mention.url)})")) + } + } + + @Test + fun validTagsAreNotMarkedUp() { + val builder = SpannableStringBuilder() + for (tag in tags) { + builder.append("#${tag.name}", URLSpan(tag.url), 0) + builder.append(" ") + } + + val markedUpContent = markupHiddenUrls(textView, builder) + for (tag in tags) { + Assert.assertFalse(markedUpContent.contains("${getDomain(tag.url)})")) + } + } + + @Test + fun invalidTagsAreNotMarkedUp() { + val builder = SpannableStringBuilder() + for (tag in tags) { + builder.append("#${tag.name}", URLSpan(tag.url), 0) + builder.append(" ") + } + + val markedUpContent = markupHiddenUrls(textView, builder) + for (tag in tags) { + Assert.assertFalse(markedUpContent.contains("${getDomain(tag.url)})")) + } + } + + @RunWith(Parameterized::class) + class UrlMatchingTests(private val url: String, private val expectedResult: Boolean) { + companion object { + @Parameterized.Parameters(name = "match_{0}") + @JvmStatic + fun data(): Iterable<Any> { + return listOf( + arrayOf("https://mastodon.foo.bar/@User", true), + arrayOf("http://mastodon.foo.bar/@abc123", true), + arrayOf("https://mastodon.foo.bar/@user/345667890345678", true), + arrayOf("https://mastodon.foo.bar/@user/3", true), + arrayOf("https://mastodon.foo.bar/users/User/statuses/43456787654678", true), + arrayOf("https://pleroma.foo.bar/users/meh3223", true), + arrayOf("https://pleroma.foo.bar/users/meh3223_bruh", true), + arrayOf("https://pleroma.foo.bar/users/2345", true), + arrayOf("https://pleroma.foo.bar/notice/9", true), + arrayOf("https://pleroma.foo.bar/notice/9345678", true), + arrayOf("https://pleroma.foo.bar/notice/wat", true), + arrayOf("https://pleroma.foo.bar/notice/9qTHT2ANWUdXzENqC0", true), + arrayOf("https://pleroma.foo.bar/objects/abcdef-123-abcd-9876543", true), + arrayOf("https://misskey.foo.bar/notes/mew", true), + arrayOf("https://misskey.foo.bar/notes/1421564653", true), + arrayOf("https://misskey.foo.bar/notes/qwer615985ddf", true), + arrayOf("https://friendica.foo.bar/profile/user", true), + arrayOf("https://friendica.foo.bar/profile/uSeR", true), + arrayOf("https://friendica.foo.bar/profile/user_user", true), + arrayOf("https://friendica.foo.bar/profile/123", true), + arrayOf("https://friendica.foo.bar/display/abcdef-123-abcd-9876543", true), + arrayOf("https://google.com/", false), + arrayOf("https://mastodon.foo.bar/@User?foo=bar", false), + arrayOf("https://mastodon.foo.bar/@User#foo", false), + arrayOf("http://mastodon.foo.bar/@", false), + arrayOf("http://mastodon.foo.bar/@/345678", false), + arrayOf("https://mastodon.foo.bar/@user/345667890345678/", false), + arrayOf("https://mastodon.foo.bar/@user/3abce", false), + arrayOf("https://pleroma.foo.bar/users/", false), + arrayOf("https://pleroma.foo.bar/users/meow/", false), + arrayOf("https://pleroma.foo.bar/users/@meow", false), + arrayOf("https://pleroma.foo.bar/notices/123456", false), + arrayOf("https://pleroma.foo.bar/notice/@neverhappen/", false), + arrayOf("https://pleroma.foo.bar/object/abcdef-123-abcd-9876543", false), + arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543", false), + arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd-9876543/", false), + arrayOf("https://pleroma.foo.bar/objects/xabcdef-123-abcd_9876543", false), + arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd-9876543", false), + arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd-9876543/", false), + arrayOf("https://friendica.foo.bar/display/xabcdef-123-abcd_9876543", false), + arrayOf("https://friendica.foo.bar/profile/@mew", false), + arrayOf("https://friendica.foo.bar/profile/@mew/", false), + arrayOf("https://misskey.foo.bar/notes/@nyan", false), + arrayOf("https://misskey.foo.bar/notes/NYAN123", false), + arrayOf("https://misskey.foo.bar/notes/meow123/", false), + arrayOf("https://pixelfed.social/p/connyduck/391263492998670833", true), + arrayOf("https://pixelfed.social/connyduck", true), + arrayOf("https://gts.foo.bar/@goblin/statuses/01GH9XANCJ0TA8Y95VE9H3Y0Q2", true), + arrayOf("https://gts.foo.bar/@goblin", true), + arrayOf("https://foo.microblog.pub/o/5b64045effd24f48a27d7059f6cb38f5", true), + arrayOf("https://bookwyrm.foo.bar/user/User", true), + arrayOf("https://bookwyrm.foo.bar/user/User/comment/123456", true) + ) + } + } + + @Test + fun test() { + Assert.assertEquals(expectedResult, looksLikeMastodonUrl(url)) + } + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/util/LocaleUtilsTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/LocaleUtilsTest.kt new file mode 100644 index 0000000..259980f --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/util/LocaleUtilsTest.kt @@ -0,0 +1,82 @@ +package com.keylesspalace.tusky.util + +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.os.LocaleListCompat +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.keylesspalace.tusky.db.entity.AccountEntity +import org.junit.Assert +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.robolectric.annotation.Config + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class LocaleUtilsTest { + @Test + fun initialLanguagesContainReplySelectedAppAndSystem() { + val expectedLanguages = arrayOf<String?>("yi", "tok", "da", "fr", "sv", "kab") + val languages = getMockedInitialLanguages(expectedLanguages) + Assert.assertArrayEquals(expectedLanguages, languages.subList(0, expectedLanguages.size).toTypedArray()) + } + + @Test + fun whenReplyLanguageIsNull_DefaultLanguageIsFirst() { + val defaultLanguage = "tok" + val languages = getMockedInitialLanguages(arrayOf(null, defaultLanguage, "da", "fr", "sv", "kab")) + Assert.assertEquals(defaultLanguage, languages[0]) + } + + @Test + fun initialLanguagesAreDistinct() { + val defaultLanguage = "da" + val languages = getMockedInitialLanguages(arrayOf(defaultLanguage, defaultLanguage, "fr", defaultLanguage, "kab", defaultLanguage)) + Assert.assertEquals(1, languages.count { it == defaultLanguage }) + } + + @Test + fun initialLanguageDeduplicationDoesNotReorder() { + val defaultLanguage = "da" + + Assert.assertEquals( + defaultLanguage, + getMockedInitialLanguages(arrayOf(defaultLanguage, defaultLanguage, "fr", defaultLanguage, "kab", defaultLanguage))[0] + ) + Assert.assertEquals( + defaultLanguage, + getMockedInitialLanguages(arrayOf(null, defaultLanguage, "fr", defaultLanguage, "kab", defaultLanguage))[0] + ) + } + + @Test + fun emptyInitialLanguagesAreDropped() { + val languages = getMockedInitialLanguages(arrayOf("", "", "fr", "", "kab", "")) + Assert.assertFalse(languages.any { it.isEmpty() }) + } + + private fun getMockedInitialLanguages(configuredLanguages: Array<String?>): List<String> { + val appLanguages = LocaleListCompat.forLanguageTags(configuredLanguages.slice(2 until 4).joinToString(",")) + val systemLanguages = LocaleListCompat.forLanguageTags(configuredLanguages.slice(4 until configuredLanguages.size).joinToString(",")) + + Mockito.mockStatic(AppCompatDelegate::class.java).use { appCompatDelegate -> + appCompatDelegate.`when`<LocaleListCompat> { AppCompatDelegate.getApplicationLocales() }.thenReturn(appLanguages) + + Mockito.mockStatic(LocaleListCompat::class.java).use { localeListCompat -> + localeListCompat.`when`<LocaleListCompat> { LocaleListCompat.getDefault() }.thenReturn(systemLanguages) + + return getInitialLanguages( + configuredLanguages[0], + AccountEntity( + id = 0, + domain = "foo.bar", + accessToken = "", + clientId = null, + clientSecret = null, + isActive = true, + defaultPostLanguage = configuredLanguages[1].orEmpty() + ) + ) + } + } + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/util/NumberUtilsTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/NumberUtilsTest.kt new file mode 100644 index 0000000..6b821b1 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/util/NumberUtilsTest.kt @@ -0,0 +1,70 @@ +package com.keylesspalace.tusky.util + +import java.util.Locale +import kotlin.math.pow +import org.junit.AfterClass +import org.junit.Assert +import org.junit.BeforeClass +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +class NumberUtilsTest(private val input: Long, private val want: String) { + companion object { + /** Default locale before this test started */ + private lateinit var locale: Locale + + /** + * Ensure the Locale is ENGLISH so that tests against literal strings like + * "1.0M" later, even if the test host's locale is e.g. GERMAN which would + * normally report "1,0M". + */ + @BeforeClass + @JvmStatic + fun beforeClass() { + locale = Locale.getDefault() + Locale.setDefault(Locale.ENGLISH) + } + + @AfterClass + @JvmStatic + fun afterClass() { + Locale.setDefault(locale) + } + + @Parameterized.Parameters(name = "formatNumber_{0}") + @JvmStatic + fun data(): Iterable<Any> { + return listOf( + arrayOf(0, "0"), + arrayOf(1, "1"), + arrayOf(-1, "-1"), + arrayOf(999, "999"), + arrayOf(1000, "1.0K"), + arrayOf(1500, "1.5K"), + arrayOf(-1500, "-1.5K"), + arrayOf(1000.0.pow(2).toLong(), "1.0M"), + arrayOf(1000.0.pow(3).toLong(), "1.0G"), + arrayOf(1000.0.pow(4).toLong(), "1.0T"), + arrayOf(1000.0.pow(5).toLong(), "1.0P"), + arrayOf(1000.0.pow(6).toLong(), "1.0E"), + arrayOf(3, "3"), + arrayOf(35, "35"), + arrayOf(350, "350"), + arrayOf(3500, "3.5K"), + arrayOf(-3500, "-3.5K"), + arrayOf(3500 * 1000, "3.5M"), + arrayOf(3500 * 1000.0.pow(2).toLong(), "3.5G"), + arrayOf(3500 * 1000.0.pow(3).toLong(), "3.5T"), + arrayOf(3500 * 1000.0.pow(4).toLong(), "3.5P"), + arrayOf(3500 * 1000.0.pow(5).toLong(), "3.5E") + ) + } + } + + @Test + fun test() { + Assert.assertEquals(want, formatNumber(input, 1000)) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/util/RickRollTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/RickRollTest.kt new file mode 100644 index 0000000..9a5e8f8 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/util/RickRollTest.kt @@ -0,0 +1,36 @@ +package com.keylesspalace.tusky.util + +import android.app.Activity +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.annotation.Config + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class RickRollTest { + private lateinit var activity: Activity + + @Before + fun setupActivity() { + val controller = Robolectric.buildActivity(Activity::class.java) + activity = controller.get() + } + + @Test + fun testShouldRickRoll() { + listOf("gab.Com", "social.gab.ai", "whatever.GAB.com").forEach { + rollableDomain -> + assertTrue(shouldRickRoll(activity, rollableDomain)) + } + + listOf("chaos.social", "notgab.com").forEach { + notRollableDomain -> + assertFalse(shouldRickRoll(activity, notRollableDomain)) + } + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/util/SmartLengthInputFilterTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/SmartLengthInputFilterTest.kt new file mode 100644 index 0000000..5b6f417 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/util/SmartLengthInputFilterTest.kt @@ -0,0 +1,103 @@ +package com.keylesspalace.tusky.util + +import android.text.SpannableStringBuilder +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class SmartLengthInputFilterTest { + + @Test + fun shouldNotTrimStatusWithLength0() { + assertFalse(shouldTrimStatus(SpannableStringBuilder(""))) + } + + @Test + fun shouldNotTrimStatusWithLength10() { + assertFalse(shouldTrimStatus(SpannableStringBuilder("0123456789"))) + } + + @Test + fun shouldNotTrimStatusWithLength500() { + assertFalse( + shouldTrimStatus( + SpannableStringBuilder( + "u1Pc5TbDVYFnzIdqlQkb3xuZ2S61fFD1K4u" + + "cb3q40dnELjAsWxnSH59jqly249Spr0Vod029zfwFHYQ0PkBCNQ7tuk90h6aY661RFC7vhIKJna4yDYOBFj" + + "RR9u0CsUa6vlgEE5yUrk5LKn3bmnnzRCXmU6HyT2bFu256qoUWbmMQ6GFXUXjO28tru8Q3UiXKLgrotKdSH" + + "mmqPwQgtatbMykTW4RZdKTE46nzlbD3mXHdWQkf4uVPYhVT1CMvVbCPMaimfQ0xuU8CpxyVqA8a6lCL3YX9" + + "pNnZjD7DoCg2FCejANnjXsTF6vuqPSHjQZDjy696nSAFy95p9kBeJkc70fHzX5TcfUqSaNtvx3LUtpIkwh4" + + "q2EYmKISPsxlANaspEMPuX6r9fSACiEwmHsitZkp4RMKZq5NqRsGPCiAXcNIN3jj9fCYVGxUwVxVeCescDG" + + "5naEEszIR3FT1RO4MSn9c2ZZi0UdLizd8ciJAIuwwmcVyYyyM4" + ) + ) + ) + } + + @Test + fun shouldNotTrimStatusWithLength666() { + assertFalse( + shouldTrimStatus( + SpannableStringBuilder( + "hIAXqY7DYynQGcr3zxcjCjNZFcdwAzwnWv" + + "NHONtT55rO3r2faeMRZLTG3JlOshq8M1mtLRn0Ca8M9w82nIjJDm1jspxhFc4uLFpOjb9Gm2BokgRftA8ih" + + "pv6wvMwF5Fg8V4qa8GcXcqt1q7S9g09S3PszCXG4wnrR6dp8GGc9TqVArgmoLSc9EVREIRcLPdzkhV1WWM9" + + "ZWw7josT27BfBdMWk0ckQkClHAyqLtlKZ84WamxK2q3NtHR5gr7ohIjU8CZoKDjv1bA8ZI8wBesyOhqbmHf" + + "0Ltypq39WKZ63VTGSf5Dd9kuTEjlXJtxZD1DXH4FFplY45DH5WuQ61Ih5dGx0WFEEVb1L3aku3Ht8rKG7YU" + + "bOPeanGMBmeI9YRdiD4MmuTUkJfVLkA9rrpRtiEYw8RS3Jf9iqDkTpES9aLQODMip5xTsT4liIcUbLo0Z1d" + + "NhHk7YKubigNQIm1mmh2iU3Q0ZEm8TraDpKu2o27gIwSKbAnTllrOokprPxWQWDVrN9bIliwGHzgTKPI5z8" + + "gUybaqewxUYe12GvxnzqpfPFvvHricyZAC9i6Fkil5VmFdae75tLFWRBfE8Wfep0dSjL751m2yzvzZTc6uZ" + + "RTcUiipvl42DaY8Z5eG2b6xPVhvXshMORvHzwhJhPkHSbnwXX5K" + ) + ) + ) + } + + @Test + fun shouldTrimStatusWithLength667() { + assertTrue( + shouldTrimStatus( + SpannableStringBuilder( + "hIAXqY7DYynQGcr3zxcjCjNZFcdwAzwnWv" + + "NHONtT55rO3r2faeMRZLTG3JlOshq8M1mtLRn0Ca8M9w82nIjJDm1jspxhFc4uLFpOjb9Gm2BokgRftA8ih" + + "pv6wvMwF5Fg8V4qa8GcXcqt1q7S9g09S3PszCXG4wnrR6dp8GGc9TqVArgmoLSc9EVREIRcLPdzkhV1WWM9" + + "ZWw7josT27BfBdMWk0ckQkClHAyqLtlKZ84WamxK2q3NtHR5gr7ohIjU8CZoKDjv1bA8ZI8wBesyOhqbmHf" + + "0Ltypq39WKZ63VTGSf5Dd9kuTEjlXJtxZD1DXH4FFplY45DH5WuQ61Ih5dGx0WFEEVb1L3aku3Ht8rKG7YU" + + "bOPeanGMBmeI9YRdiD4MmuTUkJfVLkA9rrpRtiEYw8RS3Jf9iqDkTpES9aLQODMip5xTsT4liIcUbLo0Z1d" + + "NhHk7YKubigNQIm1mmh2iU3Q0ZEm8TraDpKu2o27gIwSKbAnTllrOokprPxWQWDVrN9bIliwGHzgTKPI5z8" + + "gUybaqewxUYe12GvxnzqpfPFvvHricyZAC9i6Fkil5VmFdae75tLFWRBfE8Wfep0dSjL751m2yzvzZTc6uZ" + + "RTcUiipvl42DaY8Z5eG2b6xPVhvXshMORvHzwhJhPkHSbnwXX5K1" + ) + ) + ) + } + + @Test + fun shouldTrimStatusWithLength1000() { + assertTrue( + shouldTrimStatus( + SpannableStringBuilder( + "u1Pc5TbDVYFnzIdqlQkb3xuZ2S61fFD1K4u" + + "cb3q40dnELjAsWxnSH59jqly249Spr0Vod029zfwFHYQ0PkBCNQ7tuk90h6aY661RFC7vhIKJna4yDYOBFj" + + "RR9u0CsUa6vlgEE5yUrk5LKn3bmnnzRCXmU6HyT2bFu256qoUWbmMQ6GFXUXjO28tru8Q3UiXKLgrotKdSH" + + "mmqPwQgtatbMykTW4RZdKTE46nzlbD3mXHdWQkf4uVPYhVT1CMvVbCPMaimfQ0xuU8CpxyVqA8a6lCL3YX9" + + "pNnZjD7DoCg2FCejANnjXsTF6vuqPSHjQZDjy696nSAFy95p9kBeJkc70fHzX5TcfUqSaNtvx3LUtpIkwh4" + + "q2EYmKISPsxlANaspEMPuX6r9fSACiEwmHsitZkp4RMKZq5NqRsGPCiAXcNIN3jj9fCYVGxUwVxVeCescDG" + + "5naEEszIR3FT1RO4MSn9c2ZZi0UdLizd8ciJAIuwwmcVyYyyM4" + + "u1Pc5TbDVYFnzIdqlQkb3xuZ2S61fFD1K4u" + + "cb3q40dnELjAsWxnSH59jqly249Spr0Vod029zfwFHYQ0PkBCNQ7tuk90h6aY661RFC7vhIKJna4yDYOBFj" + + "RR9u0CsUa6vlgEE5yUrk5LKn3bmnnzRCXmU6HyT2bFu256qoUWbmMQ6GFXUXjO28tru8Q3UiXKLgrotKdSH" + + "mmqPwQgtatbMykTW4RZdKTE46nzlbD3mXHdWQkf4uVPYhVT1CMvVbCPMaimfQ0xuU8CpxyVqA8a6lCL3YX9" + + "pNnZjD7DoCg2FCejANnjXsTF6vuqPSHjQZDjy696nSAFy95p9kBeJkc70fHzX5TcfUqSaNtvx3LUtpIkwh4" + + "q2EYmKISPsxlANaspEMPuX6r9fSACiEwmHsitZkp4RMKZq5NqRsGPCiAXcNIN3jj9fCYVGxUwVxVeCescDG" + + "5naEEszIR3FT1RO4MSn9c2ZZi0UdLizd8ciJAIuwwmcVyYyyM4" + ) + ) + ) + } +} diff --git a/app/src/test/java/com/keylesspalace/tusky/util/TimestampUtilsTest.kt b/app/src/test/java/com/keylesspalace/tusky/util/TimestampUtilsTest.kt new file mode 100644 index 0000000..1375b20 --- /dev/null +++ b/app/src/test/java/com/keylesspalace/tusky/util/TimestampUtilsTest.kt @@ -0,0 +1,29 @@ +package com.keylesspalace.tusky.util + +import android.content.Context +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.keylesspalace.tusky.R +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.robolectric.annotation.Config + +private const val STATUS_CREATED_AT_NOW = "test" + +@Config(sdk = [28]) +@RunWith(AndroidJUnit4::class) +class TimestampUtilsTest { + private val ctx: Context = mock { + on { getString(R.string.status_created_at_now) } doReturn STATUS_CREATED_AT_NOW + } + + @Test + fun shouldShowNowForSmallTimeSpans() { + assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 0, 300)) + assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 300, 0)) + assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 501, 0)) + assertEquals(STATUS_CREATED_AT_NOW, getRelativeTimeSpanString(ctx, 0, 999)) + } +} diff --git a/assets/avatar_default.svg b/assets/avatar_default.svg new file mode 100644 index 0000000..4db822c --- /dev/null +++ b/assets/avatar_default.svg @@ -0,0 +1,64 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:cc="http://creativecommons.org/ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns="http://www.w3.org/2000/svg" id="svg8" version="1.1" viewBox="0 0 211.66666 211.66667" + height="800" width="800"> + <defs + id="defs2" /> + <metadata + id="metadata5"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + <dc:title></dc:title> + </cc:Work> + </rdf:RDF> + </metadata> + <rect + ry="25" + y="0" + x="0" + height="211.66667" + width="211.66667" + id="rect4493" + style="opacity:1;fill:#d9e1e8;fill-opacity:1;fill-rule:nonzero;stroke:#7c95b6;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" /> + <g + style="display:inline" + transform="translate(0,-85.333317)" + id="layer1"> + <g + transform="matrix(1.2,0,0,1.2,-19.120545,-34.579764)" + id="g5131"> + <path + id="path5100" + d="m 132.28325,194.37945 c 9.73293,3.04024 10.32027,7.55178 19.0878,-0.75596 2.96859,-3.40402 3.33739,-7.87857 4.40651,-10.28376 4.07802,-9.17426 10.47436,-13.09128 12.41343,-22.60016 0.39388,-13.4991 -6.94563,-22.1565 -19.65476,-24.75744 -7.77531,-0.67136 -15.40514,1.27604 -22.67857,9.63839 z" + style="display:inline;fill:#9baec8;fill-opacity:0.58823529;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + <path + id="path5122" + d="m 127.94502,198.06471 c 2.45762,7.8996 4.79594,14.53489 11.15029,17.95388 -1.92137,1.41168 -3.27579,1.32004 -5.19717,1.5119 -7.12025,-1.59325 -11.49199,-4.61664 -16.34747,-9.73288 z" + style="fill:#c1cddb;fill-opacity:1;stroke:#c1cddb;stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" /> + <path + style="display:inline;fill:#9baec8;fill-opacity:0.58823529;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + d="m 75.973214,194.37945 c -9.732929,3.04024 -10.320271,7.55178 -19.087797,-0.75596 -2.968597,-3.40402 -3.337392,-7.87857 -4.406513,-10.28376 -4.078022,-9.17426 -10.474359,-13.09128 -12.41343,-22.60016 -0.39388,-13.4991 6.945626,-22.1565 19.654764,-24.75744 7.77531,-0.67136 15.405133,1.27604 22.67857,9.63839 z" + id="path5081" /> + <path + style="fill:#c1cddb;fill-opacity:1;stroke:#c1cddb;stroke-width:3.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:fill markers stroke" + d="m 79.374999,198.06471 c -2.457624,7.8996 -4.79594,14.53489 -11.150297,17.95388 1.921379,1.41168 3.275793,1.32004 5.197172,1.5119 7.120253,-1.59325 11.491994,-4.61664 16.347469,-9.73288 z" + id="path5120" /> + <path + style="display:inline;fill:#a3b6cf;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:0" + d="m 103.84896,136.73793 c 15.13827,0.82979 26.87348,9.00914 31.46629,26.64747 1.61503,8.73401 0.93145,18.07152 -1.51153,27.59211 -2.81485,8.52159 -9.07481,14.90009 -15.875,20.41064 -0.98147,6.60405 -0.94269,6.47393 -2.8349,10.48884 -5.90477,11.31517 -13.06111,13.95805 -18.616873,16.5918 -5.599281,1.63795 -10.434034,1.74685 -15.29848,1.91514 l 0.181157,-11.13638 c 2.334689,-0.31211 4.733838,-0.30191 6.830423,-1.80454 1.838528,-1.29054 2.59012,-2.79846 3.18562,-4.3376 1.170265,-4.15048 1.097036,-7.47196 -7.2e-5,-10.11086 -7.934118,-5.22706 -14.66656,-13.49545 -17.481399,-22.01704 -2.442979,-9.52059 -3.126557,-18.8581 -1.511535,-27.59211 4.59282,-17.63833 16.328014,-25.81768 31.466299,-26.64747 z" + id="path5087" /> + <path + style="display:inline;fill:none;stroke:#7c95b6;stroke-width:2.08333325;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 80.418363,185.02591 c 4.018619,-8.40248 10.790056,-6.52483 13.221605,0.12474" + id="path5096" /> + <path + id="path5118" + d="m 128.11651,185.02591 c -4.01862,-8.40248 -10.79006,-6.52483 -13.22161,0.12474" + style="display:inline;fill:none;stroke:#7c95b6;stroke-width:2.08333325;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" /> + </g> + </g> +</svg> diff --git a/assets/fdroid_badge.png b/assets/fdroid_badge.png new file mode 100644 index 0000000..23af0e4 Binary files /dev/null and b/assets/fdroid_badge.png differ diff --git a/assets/tusky_banner.xcf b/assets/tusky_banner.xcf new file mode 100644 index 0000000..55551d1 Binary files /dev/null and b/assets/tusky_banner.xcf differ diff --git a/bitrise.yml b/bitrise.yml new file mode 100644 index 0000000..a7627ac --- /dev/null +++ b/bitrise.yml @@ -0,0 +1,149 @@ +format_version: "6" +default_step_lib_source: https://github.com/bitrise-io/bitrise-steplib.git +project_type: android +trigger_map: +- push_branch: develop + workflow: nightly +- pull_request_source_branch: '*' + workflow: primary +- tag: '*' + workflow: release +workflows: + nightly: + steps: + - set-java-version@1: + inputs: + - set_java_version: '17' + - activate-ssh-key: + run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' + - git-clone@8.0: {} + - cache-pull@2.7: {} + - install-missing-android-tools: + inputs: + - gradlew_path: $PROJECT_LOCATION/gradlew + - change-android-versioncode-and-versionname@1.3: {} + - gradle-runner@2: + inputs: + - apk_file_include_filter: '*.aab' + - gradlew_path: ./gradlew + - gradle_options: "" + - gradle_task: ktlintCheck lintGreenDebug testGreenReleaseUnitTest bundleGreenRelease + - sign-apk@1: + inputs: + - apk_path: $BITRISE_AAB_PATH + - script: + inputs: + - content: | + #!/usr/bin/env bash + + # write the git log to a file for the deploy step to pick up + git log -3 --pretty=%B | head -c 500 > whatsnew-en-US + - google-play-deploy@3.7: + inputs: + - apk_path: $BITRISE_SIGNED_APK_PATH + - package_name: com.keylesspalace.tusky.test + - track: production + - app_path: $BITRISE_SIGNED_AAB_PATH + - whatsnews_dir: ./ + - service_account_json_key_path: $TUSKY_SERVICE_ACC_URL + - deploy-to-bitrise-io@2.1: {} + - cache-push@2.7: {} + primary: + steps: + - set-java-version@1: + inputs: + - set_java_version: '17' + - activate-ssh-key: + run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' + - git-clone: {} + - cache-pull@2.7: {} + - install-missing-android-tools: + inputs: + - gradlew_path: $PROJECT_LOCATION/gradlew + - gradle-runner@2: + inputs: + - app_file_include_filter: |- + *.apk + *.aab + - app_file_exclude_filter: |2+ + + - test_apk_file_include_filter: "" + - mapping_file_include_filter: "" + - retry_on_failure: "no" + - gradlew_path: ./gradlew + - gradle_options: --no-daemon + - gradle_task: ktlintCheck lintGreenDebug + - android-unit-test@1.0: + inputs: + - project_location: $PROJECT_LOCATION + - module: app + - variant: greenDebug + - android-build: + inputs: + - variant: greenDebug + - module: app + - deploy-to-bitrise-io@2.1: + inputs: + - debug_mode: "true" + - notify_user_groups: none + - cache-push@2.7: {} + release: + steps: + - set-java-version@1: + inputs: + - set_java_version: '17' + - activate-ssh-key: + run_if: '{{getenv "SSH_RSA_PRIVATE_KEY" | ne ""}}' + - git-clone: {} + - cache-pull@2.7: {} + - install-missing-android-tools@3.1: + inputs: + - gradlew_path: $PROJECT_LOCATION/gradlew + - gradle-runner@2.0: + inputs: + - apk_file_include_filter: "" + - gradlew_path: ./gradlew + - gradle_task: assembleBlueRelease bundleBlueRelease + - sign-apk: + inputs: + - debuggable_permitted: "false" + - keystore_alias: $TUSKY_RELEASE_KEY_NAME + - private_key_password: $TUSKY_RELEASE_KEY_PASSWORD + - verbose_log: "true" + - android_app: $BITRISE_APK_PATH|$BITRISE_AAB_PATH + - apk_path: "" + - deploy-to-bitrise-io@2.1: + inputs: + - generate_universal_apk_if_none: "false" + - script@1: + inputs: + - content: | + #!/usr/bin/env bash + # find the newest english changelog, write it to a file for the deploy step to pick up + + changelog_file=$(ls -1 fastlane/metadata/android/en-US/changelogs | sort -V -r | head -n 1) + cat fastlane/metadata/android/en-US/changelogs/$changelog_file >> whatsnew-en-US + - google-play-deploy@3: + inputs: + - app_path: $BITRISE_AAB_PATH + - track: internal + - service_account_json_key_path: $TUSKY_SERVICE_ACC_URL + - package_name: com.keylesspalace.tusky + - cache-push@2.7: {} +app: + envs: + - opts: + is_expand: false + PROJECT_LOCATION: . + - opts: + is_expand: false + MODULE: app + - opts: + is_expand: false + BUILD_VARIANT: GreenDebug + - opts: + is_expand: false + TEST_VARIANT: GreenDebug +meta: + bitrise.io: + stack: linux-docker-android-20.04 diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..6fe4a06 --- /dev/null +++ b/build.gradle @@ -0,0 +1,29 @@ +plugins { + alias(libs.plugins.android.application) apply false + alias(libs.plugins.google.ksp) apply false + alias(libs.plugins.hilt.android) apply false + alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.parcelize) apply false + alias(libs.plugins.ktlint) apply false +} + +allprojects { + apply plugin: libs.plugins.ktlint.get().pluginId + + plugins.withType(JavaBasePlugin).configureEach { + java { + toolchain.languageVersion = JavaLanguageVersion.of(21) + } + } + + // Required for Hilt to use the toolchain + tasks.withType(JavaCompile).configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(21) + } + } +} + +tasks.register('clean') { + delete layout.buildDirectory +} diff --git a/doc/PaymentPolicy.md b/doc/PaymentPolicy.md new file mode 100644 index 0000000..ac5cfee --- /dev/null +++ b/doc/PaymentPolicy.md @@ -0,0 +1,34 @@ +<h1>Payment Policy</h1> + +We provide payment for work done on the Tusky project, including but not limited to, Coding, Code Review, Design, and User Support. We also reimburse all costs necessary to keep the project running, e.g. servers, hosting, domain renewals, and the occasional merchandise drop to promote the project. + +We primarily use our budget to enable contributors who would otherwise not be able to contribute, and prioritize payments to such need-based contributors. + +<ul> + <li>Payments for one-off contributions are welcome, but the scope of the work must be discussed and approved in our Matrix-based <a href="https://matrix.to/#/#Tusky-Code:matrix.org" target="_new">Tusky Code room</a> prior to the work commencing.</li> + <ul> + <li>Payment amounts for one-off contributions will be determined based on the scope of the work and amount of funds available, both of which will be discussed and agreed upon by the contributor and team up front. The payment amount will be considered approved with the sign-off of one of our <a href="https://opencollective.com/tusky#category-ABOUT" target="_new">OpenCollective Admins</a> after the discussion has occurred.</li> + </ul> + <li>Regular contributors can submit for payments based on a team-approved individual agreement.</li> + <li>Payment requests are approved by our <a href="https://opencollective.com/tusky#category-ABOUT" target="_new">OpenCollective Admins</a>. Admins <i>cannot</i> approve their own payment requests.</li> + <li>Payments have a soft cap of $500/month per contributor. Additional amounts may not be considered without a broad team consensus.</li> +</ul> + +<h2>Expense Submissions</h2> + +Payment requests should be submitted monthly for the previous month's work. A description of the work must be included. Some examples: + +<ul> + <li>September 2023: [#] hours of development work on GitHub issues #1234, #5678, #9000.</li> + <li>September 2023: [#] hours of support work updating Tusky FAQs, answering email inquiries sent to the Tusky email address, and posting updates to the official Mastodon Tusky account.</li> +</ul> + +You are not required to upload an invoice attachment (the data you submit in the expense form is sufficient) but if you would like to include an uploaded invoice, please make it out to: + +Tusky<br> +Open Source Collective<br> +440 N. Barranca Avenue #3717<br> +Covina, CA 91723, USA<br> + +<h2>Reimbursement Submissions</h2> +Reimbursements must include receipts, whether a PDF copy or photo of a paper receipt. For more information on what constitutes a valid receipt, see <a href="https://docs.oscollective.org/how-it-works/basics/invoice-and-reimbursement-examples#reimbursements" target="_new">Open Source Collective's documentation</a>. diff --git a/doc/Release.md b/doc/Release.md new file mode 100644 index 0000000..8e60f4d --- /dev/null +++ b/doc/Release.md @@ -0,0 +1,48 @@ +# Releasing Tusky + +Before each major release, make a beta for at least a week to make sure the release is well tested before being released for everybody. Minor releases can skip beta. + +This approach of having ~500 user on the nightly releases and ~5000 users on the beta releases has so far worked very well and helped to fix bugs before they could reach most users. + +## Beta + +- Make sure all new features are well tested by Nightly users and all issues addressed as good as possible. Check GitHub issues, Google Play crash reports, messages on `@Tusky@mastodon.social`, emails on `tusky@connyduck.at`, #Tusky hashtag. +- Merge the latest Weblate translations (Weblate -> Repository maintenance -> commit all changes, then merge the automatic PRs by @nailyk-weblate on GitHub) +- Update `versionCode` and `versionName` in `app/build.gradle` +- Add a new short changelog under `fastlane/metadata/android/en-US/changelogs`. Use the next versionCode as the filename. This is so translators on Weblate have the duration of the beta to translate the changelog and F-Droid users will see it in their language on the release. If another beta is released, the changelogs have to be renamed. Note that changelogs shouldn't be over 500 characters or F-Droid will truncate them. +- Merge `develop` into `main` +- Create a new [GitHub release](https://github.com/tuskyapp/Tusky/releases). + - Tag the head of `main`. + - Create an exhaustive changelog by going through all commits since the last release. + - Mark the release as being a pre-release. +- Bitrise will automatically build and upload the release to the Internal Testing track on Google Play. +- Do a quick check to make sure the build doesn't crash, e.g. by enrolling yourself into the test track. + - In case there are any problems, delete the GitHub release, fix the problems and start again +- Download the build as apk from Google Play (App Bundle Explorer -> choose the release -> Downloads -> Signed, universal APK). Attach it to the GitHub Release. +- Create a new Open Testing release on Google Play. Reuse the build from the Internal Testing track. +- Create a merge request at F-Droid. [Example](https://gitlab.com/fdroid/fdroiddata/-/merge_requests/11218) (F-Droid automatically picks up new release tags, but not beta ones. This could probably be changed somehow.) +- Announce the release + +## Full release + +- Make sure all new features are well tested by beta users and all issues addressed as good as possible. Check GitHub issues, Google Play crash reports, messages on `@Tusky@mastodon.social`, #Tusky hashtag. +- Merge the latest Weblate translations (Weblate -> Repository maintenance -> commit all changes, then merge the automatic PRs by @nailyk-weblate on GitHub) +- Update `versionCode` and `versionName` in `app/build.gradle` +- Merge `develop` into `main` +- Create a new [GitHub release](https://github.com/tuskyapp/Tusky/releases). + - Tag the head of `main`. + - Reuse the changelog from the beta release, or create a new one if this is only a minor release. +- (F-Droid will automatically detect and build the release) +- Bitrise will automatically build and upload the release to the Internal Testing track on Google Play. +- Do a quick check to make sure the build doesn't crash, e.g. by enrolling yourself into the test track. + - In case there are any problems, delete the GitHub release, fix the problems and start again +- Download the build as apk from Google Play (App Bundle Explorer -> choose the release -> Downloads -> Signed, universal APK). Attach it to the GitHub Release. +- Update the download link in the [index.html of the website](https://github.com/tuskyapp/tuskyapp.github.io/blob/main/index.html) to point to the apk attached to the GitHub release. +- Create a new full release on Google Play. Reuse the build from the Internal Testing track. +- Announce the release + +## Versioning + +Since Tusky is user facing software that has no Api, we don't use semantic versioning. Tusky version numbers only consist of two numbers major.minor with optional commit hash (nightly/test releases) or beta flag (beta releases). +- User visible changes in the release -> new major version +- Only bugfixes, new translations, refactorings or performance improvements in the release -> new minor version diff --git a/fastlane/metadata/android/ar/changelogs/61.txt b/fastlane/metadata/android/ar/changelogs/61.txt new file mode 100644 index 0000000..6eced74 --- /dev/null +++ b/fastlane/metadata/android/ar/changelogs/61.txt @@ -0,0 +1,7 @@ +توسكي الإصدار 7.0 + +- دعم عرض استطلاعات الرأي والتصويت وإشعارات الاقتراع + أزرار جديدة لتنقية لسان الإخطارات و حذف جميع الإخطارات +- حذف و أعاد صياغة تبويقاتكم +- مؤشر جديد يظهر إذا كان الحساب روبوتا على صورة الحساب (يمكن تعطيله في التفضيلات) +- ترجمات جديدة: بوكمول النرويجية والسلوفينية. diff --git a/fastlane/metadata/android/ar/changelogs/68.txt b/fastlane/metadata/android/ar/changelogs/68.txt new file mode 100644 index 0000000..58bd052 --- /dev/null +++ b/fastlane/metadata/android/ar/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +يكفل هذا التحديث التوافق مع ماستدون 3 ويحسن الأداء والاستقرار. diff --git a/fastlane/metadata/android/ar/changelogs/70.txt b/fastlane/metadata/android/ar/changelogs/70.txt new file mode 100644 index 0000000..2cda483 --- /dev/null +++ b/fastlane/metadata/android/ar/changelogs/70.txt @@ -0,0 +1 @@ +Ml diff --git a/fastlane/metadata/android/ar/full_description.txt b/fastlane/metadata/android/ar/full_description.txt new file mode 100644 index 0000000..f19a77c --- /dev/null +++ b/fastlane/metadata/android/ar/full_description.txt @@ -0,0 +1,12 @@ +توسكي أو Tusky هو تطبيق خفيف لماستدون ، خادم الشبكة الاجتماعية الحرة والمفتوحة المصدر. + +* مواد التصميم +* يدعم معظم ميزات واجهة برمجة التطبيقات الخاصة بماستدون +* يدعم الحسابات المتعددة +* مظهر داكن أو فاتح مع إمكانية التبديل بينهما تلقائيا بحسب ساعة اليوم +* المسودات - تحرير تبويقات والاحتفاظ بها لنشرها في وقت لاحق +* اختر بين مختلف أنواع الإيموجي +* يدعم جميع أحجام الشاشات +* مفتوح المصدر كليا - لا يعتمد بتاتا على مكتبات غير حرة مثل خدمات غوغل + +لمعرفة المزيد عن ماستدون ، يمكنكم زيارة https://joinmastodon.org/ diff --git a/fastlane/metadata/android/ar/short_description.txt b/fastlane/metadata/android/ar/short_description.txt new file mode 100644 index 0000000..c1d722e --- /dev/null +++ b/fastlane/metadata/android/ar/short_description.txt @@ -0,0 +1 @@ +تطبيق للشبكة الاجتماعية ماستدون يدعم الحسابات المتعددة diff --git a/fastlane/metadata/android/ar/title.txt b/fastlane/metadata/android/ar/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/ar/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/be/full_description.txt b/fastlane/metadata/android/be/full_description.txt new file mode 100644 index 0000000..d0fe765 --- /dev/null +++ b/fastlane/metadata/android/be/full_description.txt @@ -0,0 +1,12 @@ +Tusky — гэта лёгкі кліент для Mastodon, бескаштоўнай сацыяльнай сеткі з адкрытым кодам сервера. + +• Дызайн у стылі Material +• Падтрымка большасці магчымасцей Mastodon +• Падтрымка некалькіх уліковых запісаў +• Цёмна і светлая тэмы з магчымасцю аўтапераключэння ў залежнасці ад часу дня +• Чарнавікі — пачніце ствараць допіс і зберажыце яго на потым +• Выбар паміж рознымі наборамі эмодзі +• Аптымізаваны для розных памераў экрана +• Адкрыты зыходны код, ніякіх не вольных залежнасцей, як ад сэрвісаў Google. + +Каб даведацца больш пра Mastodon, наведайце https://joinmastodon.org/ diff --git a/fastlane/metadata/android/be/short_description.txt b/fastlane/metadata/android/be/short_description.txt new file mode 100644 index 0000000..8d85f20 --- /dev/null +++ b/fastlane/metadata/android/be/short_description.txt @@ -0,0 +1 @@ +Кліент сацыяльнай сеткі Mastodon з падтрымкай некалькіх уліковых запісаў diff --git a/fastlane/metadata/android/be/title.txt b/fastlane/metadata/android/be/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/be/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/bg/changelogs/61.txt b/fastlane/metadata/android/bg/changelogs/61.txt new file mode 100644 index 0000000..c6fed81 --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Поддръжка за показване на анкети, гласуване и известия за анкети +- Нови бутони за филтриране на раздела за известия и за изтриване на всички известия +- изтриване и преработване на вашите собствени публикации +- нов индикатор, който показва дали даден акаунт е бот на изображението на профила (може да бъде изключен в предпочитанията) +- Нови преводи: норвежки, букмал и словенски. diff --git a/fastlane/metadata/android/bg/changelogs/67.txt b/fastlane/metadata/android/bg/changelogs/67.txt new file mode 100644 index 0000000..9cf27a8 --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Вече можете да създавате анкети от Tusky +- Подобрено търсене +- Нова опция в Предпочитания на акаунта за винаги разширяване на предупрежденията за съдържание +- Аватарите в навигационното чекмедже вече имат закръглена квадратна форма +- Вече е възможно да докладвате за потребители, дори когато те никога не са публикували статус +- Tusky сега ще откаже да се свързва чрез връзки с чист текст на Android 6+ +- Много други малки подобрения и корекции на грешки diff --git a/fastlane/metadata/android/bg/changelogs/68.txt b/fastlane/metadata/android/bg/changelogs/68.txt new file mode 100644 index 0000000..ae09bdf --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Тази версия осигурява съвместимост с Mastodon 3 и подобрява производителността и стабилността. diff --git a/fastlane/metadata/android/bg/changelogs/70.txt b/fastlane/metadata/android/bg/changelogs/70.txt new file mode 100644 index 0000000..d1cca33 --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Вече можете да маркирате състояния и да показвате отметките си в Tusky. +- Вече можете да планирате публикации с Tusky. Имайте предвид, че избраното време трябва да бъде поне 5 минути в бъдеще. +- Вече можете да добавяте списъци към главния екран. +- Вече можете да публикувате аудио прикачени файлове с Tusky. + +И много други малки подобрения и корекции на грешки! diff --git a/fastlane/metadata/android/bg/changelogs/74.txt b/fastlane/metadata/android/bg/changelogs/74.txt new file mode 100644 index 0000000..4bcdada --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Подобрен основен интерфейс - вече можете да премествате разделите отдолу +- Когато заглушавате потребител, вече можете също да решите дали да заглушите известията му +- Вече можете да следвате колкото искате хештегове в един единствен раздел хештегове +- Подобрен е начинът, по който се показват описанията на мултимедиите, така че да работи дори за супер дълги описания + +Пълен дневник на промените: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/bg/changelogs/77.txt b/fastlane/metadata/android/bg/changelogs/77.txt new file mode 100644 index 0000000..e24a060 --- /dev/null +++ b/fastlane/metadata/android/bg/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- поддръжка за бележки в профила (функция на Mastodon 3.2.0) +- поддръжка за администраторски съобщения (функция на Mastodon 3.1.0) + +- аватарът на избрания от вас акаунт вече ще се показва в главната лента с инструменти +- щракването върху показваното име в емисия ще отвори страницата с профила на този потребител + +- много корекции на грешки и малки подобрения +- подобрени преводи diff --git a/fastlane/metadata/android/bg/full_description.txt b/fastlane/metadata/android/bg/full_description.txt new file mode 100644 index 0000000..73ce435 --- /dev/null +++ b/fastlane/metadata/android/bg/full_description.txt @@ -0,0 +1,12 @@ +Tusky е лек клиент за Mastodon, свободен сървър за социални мрежи с отворен код. + +• Материален дизайн +• Повечето приложени API на Mastodon +• Поддръжка на няколко акаунта +• Тъмна и светла тема с възможност за автоматично превключване в зависимост от часа +• Чернови - съставете публикации и ги запазете за по-късно +• Изберете между различни стилове емоджита +• Оптимизиран за всички размери на екрана +• Напълно отворен код - няма несвободни зависимости като услугите на Google + +За да научите повече за Mastodon, посетете https://joinmastodon.org/ diff --git a/fastlane/metadata/android/bg/short_description.txt b/fastlane/metadata/android/bg/short_description.txt new file mode 100644 index 0000000..d033115 --- /dev/null +++ b/fastlane/metadata/android/bg/short_description.txt @@ -0,0 +1 @@ +Клиент с няколко акаунта за социалната мрежа Mastodon diff --git a/fastlane/metadata/android/bg/title.txt b/fastlane/metadata/android/bg/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/bg/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/bn-BD/full_description.txt b/fastlane/metadata/android/bn-BD/full_description.txt new file mode 100644 index 0000000..d33c469 --- /dev/null +++ b/fastlane/metadata/android/bn-BD/full_description.txt @@ -0,0 +1,12 @@ +টাস্কি একটি ফ্রি এবং মুক্ত উৎসবিশিষ্ট সামাজিক নেটওয়ার্ক সার্ভার মাস্টোডনের জন্য একটি হালকা ক্লায়েন্ট। + +• মেটেরিয়াল নকশা +• বেশিরভাগ মাস্টডন এপিআই প্রয়োগ করা হয়েছে +• বহু অ্যাকাউন্ট সমর্থন করে +• দিনের সময় ভিত্তিতে অন্ধকার এবং হালকা রঙে পাল্টানো যায় +• খসড়া - টুট রচনা করে পরে ছাপানোর জন্য সংরক্ষণ করো +• বিভিন্ন আবেগ শৈলীর বিদ্যমান +• সমস্ত পর্দার আকারের জন্য অনূকুল +• সম্পূর্ণ মুক্ত উৎসবিশিষ্ট- গুগল পরিষেবাগুলোর মতো কোনও অ-মুক্ত নির্ভরতা নেই + +মাস্টডন সম্পর্কে আরও জানতে, দেখো https://joinmastodon.org/ diff --git a/fastlane/metadata/android/bn-BD/short_description.txt b/fastlane/metadata/android/bn-BD/short_description.txt new file mode 100644 index 0000000..a8ae79c --- /dev/null +++ b/fastlane/metadata/android/bn-BD/short_description.txt @@ -0,0 +1 @@ +মাস্টডন নামক সামাজিক নেটওয়ার্কের জন্য একাধিক অ্যাকাউন্ট সমর্থনকারী ক্লায়েন্ট diff --git a/fastlane/metadata/android/bn-BD/title.txt b/fastlane/metadata/android/bn-BD/title.txt new file mode 100644 index 0000000..40477bf --- /dev/null +++ b/fastlane/metadata/android/bn-BD/title.txt @@ -0,0 +1 @@ +টাস্কি diff --git a/fastlane/metadata/android/bn-IN/changelogs/67.txt b/fastlane/metadata/android/bn-IN/changelogs/67.txt new file mode 100644 index 0000000..d76c7e2 --- /dev/null +++ b/fastlane/metadata/android/bn-IN/changelogs/67.txt @@ -0,0 +1,9 @@ +টাস্কি সং-৯.০ + +- আপনি এখন টাস্কি থেকে পোল তৈরি করতে পারেন +- উন্নত অনুসন্ধান +- সর্বদা সামগ্রীর সতর্কতাগুলি প্রসারিত করতে অ্যাকাউন্ট পছন্দগুলিতে নতুন বিকল্প +- নেভিগেশন ড্রয়ারের অবতারগুলিতে এখন একটি বৃত্তাকার বর্গাকার আকার রয়েছে +- ব্যবহারকারীরা কোনও স্ট্যাটাস কখনও পোস্ট না করলেও এখন তাদের প্রতিবেদন করা সম্ভব +- টস্কি এখন অ্যান্ড্রয়েড 6+ এ ক্লিয়ারটেক্সট সংযোগের মাধ্যমে সংযোগ দিতে অস্বীকার করবে +- অন্যান্য অনেক ছোট উন্নতি এবং বাগ ফিক্স diff --git a/fastlane/metadata/android/bn-IN/full_description.txt b/fastlane/metadata/android/bn-IN/full_description.txt new file mode 100644 index 0000000..0584bbf --- /dev/null +++ b/fastlane/metadata/android/bn-IN/full_description.txt @@ -0,0 +1,13 @@ +টাস্কি একটি ফ্রি এবং মুক্ত উৎসবিশিষ্ট সামাজিক নেটওয়ার্ক সার্ভার মাস্টোডনের জন্য একটি হালকা ক্লায়েন্ট। + +• মেটেরিয়াল নকশা +• বেশিরভাগ মাস্টডন এপিআই প্রয়োগ করা হয়েছে +• বহু অ্যাকাউন্ট সমর্থন করে +• দিনের সময় ভিত্তিতে অন্ধকার এবং হালকা রঙে পাল্টানো যায় +• খসড়া - টুট রচনা করে পরে ছাপানোর জন্য সংরক্ষণ করো +• বিভিন্ন আবেগ শৈলীর বিদ্যমান +• সমস্ত পর্দার আকারের জন্য অনূকুল +• সম্পূর্ণ মুক্ত উৎসবিশিষ্ট- গুগল পরিষেবাগুলোর মতো কোনও অ-মুক্ত নির্ভরতা নেই + +মাস্টডন সম্পর্কে আরও জানতে, দেখো +https://joinmastodon.org/ diff --git a/fastlane/metadata/android/bn-IN/short_description.txt b/fastlane/metadata/android/bn-IN/short_description.txt new file mode 100644 index 0000000..a8ae79c --- /dev/null +++ b/fastlane/metadata/android/bn-IN/short_description.txt @@ -0,0 +1 @@ +মাস্টডন নামক সামাজিক নেটওয়ার্কের জন্য একাধিক অ্যাকাউন্ট সমর্থনকারী ক্লায়েন্ট diff --git a/fastlane/metadata/android/bn-IN/title.txt b/fastlane/metadata/android/bn-IN/title.txt new file mode 100644 index 0000000..40477bf --- /dev/null +++ b/fastlane/metadata/android/bn-IN/title.txt @@ -0,0 +1 @@ +টাস্কি diff --git a/fastlane/metadata/android/ca/changelogs/100.txt b/fastlane/metadata/android/ca/changelogs/100.txt new file mode 100644 index 0000000..bd41f89 --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Suport per a l'edició de publicacions +- Nova configuració per controlar la direcció de lectura preferida +- Visualitzacions prèvies d'imatges més grans i una nova superposició per indicar les imatges amb descripció +- Ara és possible afegir comptes a llistes des del seu perfil +i molt més diff --git a/fastlane/metadata/android/ca/changelogs/58.txt b/fastlane/metadata/android/ca/changelogs/58.txt new file mode 100644 index 0000000..fa7eb58 --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/58.txt @@ -0,0 +1,13 @@ +Tusky v6.0 + +- Els filtres de cronologia s'han mogut a Preferències del compte i se sincronitzaran amb el servidor +- Ara podeu tenir un hashtag personalitzat com a pestanya a la interfície principal +- Ara es poden editar les llistes +- Seguretat: s'ha eliminat el suport per a TLS 1.0 i TLS 1.1 i s'ha afegit suport per a TLS 1.3 a Android 6+ +- La vista de redacció ara suggerirà emojis personalitzats quan comenci a escriure +- Nova configuració del tema "seguir el tema del sistema" +- Millora de l'accessibilitat de la cronologia +- Ara Tusky ignorarà les notificacions desconegudes i ja no es bloquejarà +- Configuració nova: ara podeu anul·lar l'idioma del sistema i definir un idioma diferent a Tusky +- Noves traduccions: txec i esperanto +- Moltes altres millores i correccions diff --git a/fastlane/metadata/android/ca/changelogs/61.txt b/fastlane/metadata/android/ca/changelogs/61.txt new file mode 100644 index 0000000..2ac3a1b --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Suport per mostrar votacions, vots i notificacions de votacions +- Nous botons per filtrar la pestanya de notificacions i suprimir totes les notificacions +- Suprimeix i redirigeix els teus propis signes +- Nou indicador que mostra si un compte és un bot a la imatge de perfil (es pot desactivar a les preferències) +- Noves traduccions: norvégien bokmål i eslovè diff --git a/fastlane/metadata/android/ca/changelogs/67.txt b/fastlane/metadata/android/ca/changelogs/67.txt new file mode 100644 index 0000000..6b039ef --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Ara podeu crear enquestes a partir de Tusky +- Millora de la cerca +- Nova opció a Preferències del compte per ampliar sempre els avisos de contingut +- Els avatars del calaix de navegació tenen ara una forma quadrada arrodonida +- Ara és possible informar als usuaris fins i tot quan mai no han publicat un estat +- Ara Tusky es negarà a connectar-se a connexions de text clar a Android 6+ +- Un munt d’altres petites millores i solucions d’errors diff --git a/fastlane/metadata/android/ca/changelogs/68.txt b/fastlane/metadata/android/ca/changelogs/68.txt new file mode 100644 index 0000000..81ca68d --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Aquesta versió garanteix la compatibilitat amb Mastodon 3 i millora el rendiment i l'estabilitat. diff --git a/fastlane/metadata/android/ca/changelogs/70.txt b/fastlane/metadata/android/ca/changelogs/70.txt new file mode 100644 index 0000000..0daa156 --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Ara podeu marcar els estatuts de les adreces i enumerar les adreces d'interès a Tusky. +- Ara podeu programar toots amb Tusky. Tingueu en compte que el temps que seleccioneu ha de ser d'almenys 5 minuts en el futur. +- Ara podeu afegir llistes a la pantalla principal. +- Ja podeu publicar fitxers adjunts d'àudio amb Tusky. + +I moltes altres petites millores i solucions d’errors. diff --git a/fastlane/metadata/android/ca/changelogs/72.txt b/fastlane/metadata/android/ca/changelogs/72.txt new file mode 100644 index 0000000..7cefae1 --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/72.txt @@ -0,0 +1,9 @@ +Tusky v11.0 + +- Notificacions sobre les noves sol·licituds de seguiment quan el vostre compte està bloquejat +- Noves funcions que es poden commutar a la pantalla de Preferències: + - desactiva el desplaçament entre pestanyes + - mostreu un diàleg de confirmació abans de fer augmentar el punt de mira + - mostra les previsualitzacions d'enllaços en els terminis +- Ara es poden silenciar les converses +- més informació al changelog diff --git a/fastlane/metadata/android/ca/changelogs/74.txt b/fastlane/metadata/android/ca/changelogs/74.txt new file mode 100644 index 0000000..d9c9f9a --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Interfície principal millorada: ara podeu moure les pestanyes a la part inferior +- Quan silencieu un usuari, ara també podeu decidir si silencieu les seves notificacions +- Ara podeu seguir tants hashtags com vulgueu en una sola pestanya d'hashtag +- S'ha millorat la manera com es mostren les descripcions dels mitjans perquè funcioni fins i tot per a descripcions molt llargues + +Registre de canvis complet: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/ca/changelogs/77.txt b/fastlane/metadata/android/ca/changelogs/77.txt new file mode 100644 index 0000000..f209d23 --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- Suport per a notes de perfil (funció Mastodon 3.2.0) +- Suport per a anuncis d'administrador (funció Mastodon 3.1.0) + +- Ara es mostrarà l'avatar del vostre compte seleccionat a la barra d'eines principal +- Si feu clic al nom de visualització en una línia de temps, ara s'obrirà la pàgina de perfil d'aquest usuari + +- Moltes correccions d'errors i petites millores +- Traduccions millorades diff --git a/fastlane/metadata/android/ca/changelogs/80.txt b/fastlane/metadata/android/ca/changelogs/80.txt new file mode 100644 index 0000000..4db5efb --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Rebeu una notificació quan un usuari seguit publica: feu clic a la icona de campana del seu perfil! (funció Mastodon 3.3.0) +- La funció d'esborrany de Tusky s'ha redissenyat completament per ser més ràpida, més fàcil d'utilitzar i amb menys errors. +- S'ha afegit un nou mode de benestar que us permet limitar determinades funcions de Tusky. +- Ara Tusky pot animar emojis personalitzats. +Registre de canvis complet: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/ca/changelogs/82.txt b/fastlane/metadata/android/ca/changelogs/82.txt new file mode 100644 index 0000000..ec73bca --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Les sol·licituds de seguiment ara es mostren sempre al menú principal. +- El selector de temps per programar una publicació té ara un disseny coherent amb la resta de l'aplicació +Registre de canvis complet: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/ca/changelogs/83.txt b/fastlane/metadata/android/ca/changelogs/83.txt new file mode 100644 index 0000000..5ae0965 --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Aquesta versió corregeix un error en subtitular imatges diff --git a/fastlane/metadata/android/ca/changelogs/87.txt b/fastlane/metadata/android/ca/changelogs/87.txt new file mode 100644 index 0000000..8a0c97c --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- La lògica de càrrega de la línia de temps s'ha reescrit completament per tal de ser més ràpida, amb menys errors i més fàcil de mantenir. +- Tusky ara pot animar emojis personalitzats en format APNG i WebP animat. +- Moltes correccions d'errors +- Suport per a Android 11 +- Noves traduccions: gaèlic escocès, gallec, ucraïnès +- Traduccions millorades diff --git a/fastlane/metadata/android/ca/changelogs/89.txt b/fastlane/metadata/android/ca/changelogs/89.txt new file mode 100644 index 0000000..f7b6ea3 --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Obre com..." ara també està disponible al menú dels perfils del compte quan s'utilitzen diversos comptes +- Ara l'inici de sessió es gestiona en una WebView dins de l'aplicació +- Suport per a Android 12 +- Suport per a la nova API de configuració de la instància Mastodon +- i moltes altres petites correccions i millores diff --git a/fastlane/metadata/android/ca/changelogs/91.txt b/fastlane/metadata/android/ca/changelogs/91.txt new file mode 100644 index 0000000..a67254f --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Suport per als nous tipus de notificacions Mastodon 3.5 +- La insígnia del bot ara es veu millor i s'ajusta al tema seleccionat +- Ara es pot seleccionar el text a la vista detallada de la publicació +- S'han corregit molts errors, inclòs un que impedia els inicis de sessió a Android 6 i anteriors diff --git a/fastlane/metadata/android/ca/changelogs/94.txt b/fastlane/metadata/android/ca/changelogs/94.txt new file mode 100644 index 0000000..cbe187c --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Suport per a Unified Push. Per activar l'assistència, haureu de tornar a iniciar sessió als vostres comptes. +- El nombre de respostes a una publicació s'indica ara a les línies de temps. +- Les imatges ara es poden retallar mentre es redacta una publicació. +- Ara els perfils mostren la data en què es van crear. +- Quan es visualitza una llista, ara el títol es mostra a la barra d'eines. +- Moltes correccions d'errors +- Millores en la traducció diff --git a/fastlane/metadata/android/ca/changelogs/97.txt b/fastlane/metadata/android/ca/changelogs/97.txt new file mode 100644 index 0000000..16c922f --- /dev/null +++ b/fastlane/metadata/android/ca/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- Nova icona de l'aplicació de Dzuk https://dzuk.zone/ +- Ara podeu seguir els hashtags. Feu clic a un hashtag i després a la icona de la barra d'eines. +- Suport per a Android 13 +- nou menú desplegable a la vista de redacció per definir l'idioma d'una publicació +- La pestanya multimèdia dels perfils ara respecta les imatges sensibles i es carrega més suaument. +- Ara és possible establir el punt d'enfocament d'una imatge abans de publicar-la +- Nova opció per mostrar el vostre nom d'usuari complet a la barra d'eines diff --git a/fastlane/metadata/android/ca/full_description.txt b/fastlane/metadata/android/ca/full_description.txt new file mode 100644 index 0000000..7aacc1c --- /dev/null +++ b/fastlane/metadata/android/ca/full_description.txt @@ -0,0 +1,12 @@ +Tusky és un client lleuger per a Mastodon, un servidor de xarxa social gratuït i de codi obert. + +• Estils de "Material Design" +• S'han implementat la majoria de les API de Mastodon +• Assistència multi-compte +• Tema fosc i clar amb la possibilitat de canviar automàticament en funció de l’hora del dia +• Esborranys: composeu els toots de sortida i deseu-los per a més endavant +• Trieu entre diferents estils emoji +• Optimitzat per a totes les mides de pantalla +• Completament de codi obert: no hi ha dependències no gratuïtes com els serveis de Google + +Per obtenir més informació sobre Mastodon, visiteu https://joinmastodon.org/ diff --git a/fastlane/metadata/android/ca/short_description.txt b/fastlane/metadata/android/ca/short_description.txt new file mode 100644 index 0000000..151f74e --- /dev/null +++ b/fastlane/metadata/android/ca/short_description.txt @@ -0,0 +1 @@ +Un client multicomptes per a la xarxa social Mastodont diff --git a/fastlane/metadata/android/ca/title.txt b/fastlane/metadata/android/ca/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/ca/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/ckb/changelogs/77.txt b/fastlane/metadata/android/ckb/changelogs/77.txt new file mode 100644 index 0000000..ee35b25 --- /dev/null +++ b/fastlane/metadata/android/ckb/changelogs/77.txt @@ -0,0 +1,10 @@ +تاسکی وشانی ١٣.٠ + +- پشتگیری بۆ تێبینیەکانی پرۆفایل (تایبەتمەندی ماستۆدۆن ٣.٢.٠) +- پشتگیری لە راگەیاندنی بەڕێوەبەر (تایبەتمەندی ماستۆدۆن ٣.١.٠) + +- ئێستا ئەژمێری هەڵبژێردراوی هەژمارەکەت لە شریتی ئامڕازی سەرەکی دا پیشان دەدرێت +- کرتە کردن لەسەر ناوی پیشاندان لە هێڵی کات ئێستا لاپەڕەی پرۆفایلی ئەو بەکارهێنەرە هەڵدەدات + +- زۆر چاککردنەوەی هەڵەکان و چاککردنەوەی بچووک +- وەرگێڕانە باشەکان diff --git a/fastlane/metadata/android/ckb/full_description.txt b/fastlane/metadata/android/ckb/full_description.txt new file mode 100644 index 0000000..ab90d92 --- /dev/null +++ b/fastlane/metadata/android/ckb/full_description.txt @@ -0,0 +1,12 @@ +توسکی ئەپێکی سووکەڵە بۆ ماستۆدۆنە، خزمەتکاری تۆڕی کۆمەڵایەتی ئازاد و کراوە + +• دیزاینی ماتریالی +• زۆربەی ماستۆدۆن API جێبەجێ دەکا +• پشتیوانی هەژمارەی هەمەجۆر +• ڕووکاری تاریک و رووناک لەگەڵ ئەگەری گۆڕینی خۆکار لەسەر بنەمای کاتی رۆژ +• ڕەشنووسەکان - دروستکردنی دووتەکان و هەڵگرتنیان بۆ دواتر +• هەڵبژێرە لەنێوان شێوازە ئیمۆجییە جیاوازەکان +• باشترکراوە بۆ هەموو قەبارەی شاشە +• بەتەواوی کراوەی سەرچاوە - هیچ پشت پێبەستنێکی نائازاد وەک خزمەتگوزاریەکانی گووگڵ + +بۆ زیاتر فێربوون لەبارەی مەستوورن ، سەردانی https://joinmastodon.org/ diff --git a/fastlane/metadata/android/ckb/short_description.txt b/fastlane/metadata/android/ckb/short_description.txt new file mode 100644 index 0000000..df2f8d3 --- /dev/null +++ b/fastlane/metadata/android/ckb/short_description.txt @@ -0,0 +1 @@ +کڕیارێکی هەژماری هەمەجۆر بۆ تۆڕی کۆمەڵایەتی ماستۆدۆن diff --git a/fastlane/metadata/android/ckb/title.txt b/fastlane/metadata/android/ckb/title.txt new file mode 100644 index 0000000..57a4e89 --- /dev/null +++ b/fastlane/metadata/android/ckb/title.txt @@ -0,0 +1 @@ +تاسکی diff --git a/fastlane/metadata/android/cs/changelogs/58.txt b/fastlane/metadata/android/cs/changelogs/58.txt new file mode 100644 index 0000000..0822c58 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/58.txt @@ -0,0 +1,13 @@ +Tusky v6.0 + +- Filtry časové osy jsme přesunuli do Předvoleb účtu a budou se synchronizovat se serverem. +- Na hlavním stránce můžete mít vlastní hashtag jako kartu +- Seznamy lze upravovat +- Zabezpečení: odstraněna podpora TLS 1.0 a TLS 1.1 a přidána podpora TLS 1.3 v systému Android 6+. +- Vlastní emotikony jsou nabízeny při psaní +- Nové nastavení motivu "následovat systémový motiv" +- Vylepšená přístupnost časové osy +- Tusky teď ignoruje neznámá oznámení a už nepadá +- Nové nastavení: Nyní můžete v aplikaci Tusky nastavit jiný než systímový jazyk. +- Nové překlady: čeština (!) a esperanto +- Mnoho dalších vylepšení a oprav diff --git a/fastlane/metadata/android/cs/changelogs/61.txt b/fastlane/metadata/android/cs/changelogs/61.txt new file mode 100644 index 0000000..cec2903 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Podpora pro zobrazování anket, hlasování a oznámení o anketách +- Nová tlačítka pro filtrování v záložce oznámení a smazání všech oznámení +- Smažte a přepište své vlastní tooty +- Nový indikátor na obrázku profilu, který ukazuje, jestli je účet robot (může být vypnut v nastavení) +- Nové překlady: norština (bokmål) a slovinština. diff --git a/fastlane/metadata/android/cs/changelogs/67.txt b/fastlane/metadata/android/cs/changelogs/67.txt new file mode 100644 index 0000000..0db8b0f --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Nyní můžete z Tuskyho vytvářet ankety +- Vylepšené vyhledávání +- Nová možnost v nastavení účtu umožňuje vždy rozbalovat varování o obsahu +- Avatary v navigačním menu mají nyní zakulacené rohy +- Je nyní možné nahlašovat uživatele i pokud ještě nenapsali žádný příspěvek +- Tusky bude odteď odmítat cleartextová spojení na Androidu 6+ +- Spousta dalších malých vylepšení a oprav chyb diff --git a/fastlane/metadata/android/cs/changelogs/68.txt b/fastlane/metadata/android/cs/changelogs/68.txt new file mode 100644 index 0000000..db89108 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Toto vydání zajišťuje kompatibilitu s Mastodonem 3 a vylepšuje výkon a stabilitu. diff --git a/fastlane/metadata/android/cs/changelogs/70.txt b/fastlane/metadata/android/cs/changelogs/70.txt new file mode 100644 index 0000000..ac9aef1 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- záložky na statusy a seznam záložek. +- plánování tootů: Čas, který vyberete, musí být alespoň 5 minut v budoucnosti. +- seznamy na hlavní obrazovce. +- odesílání zvukových příloh. + +A spousta dalších drobných vylepšení a oprav chyb! diff --git a/fastlane/metadata/android/cs/changelogs/72.txt b/fastlane/metadata/android/cs/changelogs/72.txt new file mode 100644 index 0000000..22da5ff --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- ipozornění na nové žádosti o sledování, když je váš účet uzamčen +- nové funkce, které lze zapínat v předvolebách: + - swajpování mezi kartami + - potvrzení boostu + - zobrazení náhledů odkazů v časových osách +- konverzace lze ztlumit +- Výsledky ankety se nyní budou počítat na základě počtu hlasujících, a ne na základě celkového počtu hlasů, což usnadňuje přehlednost anket s více možnostmi volby. +- oprava mnoha chyb, z nichž většina se týká psaní tootů +- vylepšené překlady diff --git a/fastlane/metadata/android/cs/changelogs/74.txt b/fastlane/metadata/android/cs/changelogs/74.txt new file mode 100644 index 0000000..c62a7e4 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Vylepšená hlavní obrazovka - karty lze nyní přesunout do spodní části. +- Při ztlumení uživatele se nyní můžete také rozhodnout, zda chcete ztlumit jeho oznámení +- Nyní můžete sledovat libovolný počet hashtagů na jedné kartě hashtagů +- Vylepšený způsob zobrazování popisů médií, takže funguje i u superdlouhých popisů + +Úplný seznam změn: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/cs/changelogs/77.txt b/fastlane/metadata/android/cs/changelogs/77.txt new file mode 100644 index 0000000..54b71b9 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- podpora poznámek k profilům (Mastodon 3.2.0) +- podpora oznámení pro správce (Mastodon 3.1.0) + +- avatar vybraného účtu se nyní zobrazuje na hlavním panelu nástrojů +- kliknutím na zobrazené jméno na časové ose se nyní otevře profilová stránka daného uživatele + +- mnoho oprav chyb a drobných vylepšení +- vylepšené překlady diff --git a/fastlane/metadata/android/cs/changelogs/80.txt b/fastlane/metadata/android/cs/changelogs/80.txt new file mode 100644 index 0000000..d7c8c10 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/80.txt @@ -0,0 +1,8 @@ +Tusky v14.0 + +- upozornění na příspěvky sledovaného uživatele - klikněte na ikonu zvonku na jeho profilu! (funkce Mastodon 3.3.0) +- redesign funkce návrhu. Teď je rychlejší, uživatelsky přívětivější a méně chybová. +- Byl přidán nový zen režim, který umožňuje omezit některé funkce Tusky. +- Tusky nyní umí animovat vlastní emotikony. + +Úplný seznam změn: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/cs/changelogs/82.txt b/fastlane/metadata/android/cs/changelogs/82.txt new file mode 100644 index 0000000..29dcb1c --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Žádosti o sledování se vždy zobrazují v hlavní nabídce. +- Výběr času pro naplánování příspěvku má teď design odpovídající zbytku aplikace. +Úplný seznam změn: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/cs/changelogs/83.txt b/fastlane/metadata/android/cs/changelogs/83.txt new file mode 100644 index 0000000..ae0ac52 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Toto vydání opravuje pád při popisování obrázků diff --git a/fastlane/metadata/android/cs/changelogs/87.txt b/fastlane/metadata/android/cs/changelogs/87.txt new file mode 100644 index 0000000..746d196 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- Logika načítání časové osy byla kompletně přepsána, aby byla rychlejší, méně chybová a jednodušší na údržbu. +- Tusky nyní umí animovat vlastní emotikony ve formátu APNG a Animated WebP. +- mnoho opravených chyb +- podpora systému Android 11 +- nové překlady: skotská gaelština, galicijština, ukrajinština. +- vylepšené překlady diff --git a/fastlane/metadata/android/cs/changelogs/89.txt b/fastlane/metadata/android/cs/changelogs/89.txt new file mode 100644 index 0000000..f06d840 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Otevřít jako..." je nyní k dispozici také v nabídce profilů účtů při použití více účtů. +- Přihlášení je nyní zpracováváno ve webovém zobrazení v rámci aplikace +- podpora systému Android 12 +- podpora nového API pro konfiguraci instancí Mastodon +- a mnoho dalších drobných oprav a vylepšení diff --git a/fastlane/metadata/android/cs/changelogs/91.txt b/fastlane/metadata/android/cs/changelogs/91.txt new file mode 100644 index 0000000..ab56d3d --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- podpora nových typů oznámení (Mastodon 3.5) +- Odznak bota nyní vypadá lépe a přizpůsobuje se zvolenému tématu. +- V zobrazení detailu příspěvku lze nyní vybrat text +- Opraveno mnoho chyb, včetně jedné, která znemožňovala přihlášení v systému Android 6 a nižším. diff --git a/fastlane/metadata/android/cs/changelogs/94.txt b/fastlane/metadata/android/cs/changelogs/94.txt new file mode 100644 index 0000000..4808320 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- podpora Unified Push: Pro aktivaci podpory se musíte znovu přihlásit ke svým účtům. +- Počet odpovědí na příspěvek se nyní zobrazuje v časových osách. +- Obrázky lze nyní při vytváření příspěvku oříznout. +- U profilů se nyní zobrazuje datum jejich vytvoření. +- Při prohlížení seznamu se nyní na panelu nástrojů zobrazuje jeho název. +- Mnoho opravených chyb +- Vylepšení překladů diff --git a/fastlane/metadata/android/cs/changelogs/97.txt b/fastlane/metadata/android/cs/changelogs/97.txt new file mode 100644 index 0000000..c732595 --- /dev/null +++ b/fastlane/metadata/android/cs/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- Nová ikona aplikace od Dzuk https://dzuk.zone/ +- Nyní můžete sledovat hashtagy. Klikněte na hashtag a poté na ikonu v panelu nástrojů. +- podpora systému Android 13 +- Nová rozbalovací nabídka v při psaní příspěvku, která umožňuje nastavit jazyk příspěvku. +- Karta médií v profilech nyní respektuje citlivá média a načítá se plynuleji. +- Před odesláním příspěvku je nyní možné nastavit bod střed zvětšení obrázku. +- nová možnost zobrazení celého uživatelského jména v panelu nástrojů diff --git a/fastlane/metadata/android/cs/full_description.txt b/fastlane/metadata/android/cs/full_description.txt new file mode 100644 index 0000000..db98fc3 --- /dev/null +++ b/fastlane/metadata/android/cs/full_description.txt @@ -0,0 +1,12 @@ +Tusky je odlehčený klient pro Mastodon, svobodný a otevřený server pro sociální síť. + +• Material Design +• Implementována většina API Mastodonu +• Podpora více účtů +• Tmavý a světlý motiv s možností automatického přepínání podle denní doby +• Koncepty – pište tooty a uložte je na později +• Vyberte si mezi různými styly emoji +• Optimalizováno pro obrazovky všech velikostí +• Zcela otevřený kód – žádný nesvobodný uzavřený software jako služby Google + +Chcete-li o Mastodonu vědět více, navštivte https://joinmastodon.org/ diff --git a/fastlane/metadata/android/cs/short_description.txt b/fastlane/metadata/android/cs/short_description.txt new file mode 100644 index 0000000..99eacd2 --- /dev/null +++ b/fastlane/metadata/android/cs/short_description.txt @@ -0,0 +1 @@ +Víceúčtový klient pro sociální síť Mastodon diff --git a/fastlane/metadata/android/cs/title.txt b/fastlane/metadata/android/cs/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/cs/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/cy/changelogs/100.txt b/fastlane/metadata/android/cy/changelogs/100.txt new file mode 100644 index 0000000..2d73863 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Cefnogaeth ar gyfer golygu post +- Gosodiad newydd i ddewis y cyfeiriad darllen a ffefrir +- Rhagluniau cyfryngau mwy a throshaen newydd i nodi cyfryngau gyda disgrifiad +- Mae bellach yn bosibl ychwanegu cyfrifon at restrau o'u proffil +a llawer mwy diff --git a/fastlane/metadata/android/cy/changelogs/103.txt b/fastlane/metadata/android/cy/changelogs/103.txt new file mode 100644 index 0000000..8ffae98 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/103.txt @@ -0,0 +1,18 @@ +Tusky 22.0 beta 1 + +Nodweddion sydd yn ychwanegu: + +- Gweld hashnodau tueddol +- Golygu disgrifiad delweddau a phwynt ffocws +- Dewislen "Adnewyddu" er mwyn hygyrchedd +- Cefnogi hidlydd Mastodon v4 +- Dangos gwahaniaethau manwl pryd mae neges yn cael ei golwg +- Opsiwn i ddangos ystadegau neges yn y ffrwd + +Cywiriadau sydd yn ychwanegu: + +- Dangos rheolau'r chwaraewr wrth chwarae sain +- Cyfrifo hyd y neges yn gywir +- Cyhoeddi disgrifiad delweddau pob tro + +A llawer mwy diff --git a/fastlane/metadata/android/cy/changelogs/104.txt b/fastlane/metadata/android/cy/changelogs/104.txt new file mode 100644 index 0000000..d6383bc --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/104.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 2 + +Cywiriadau sydd yn ychwanegu: + +- Llwytho hysbysiadau'n gyflymach +- Dangos 0/1/1+ ar gyfer ymatebion eto +- Dangos teitlau hidlydd, yn lle allweddeiriau hidlydd, yn negeseuon wedi'u hidlo +- Cywirwyd gwallau sydd yn perthyn ag y posibilrwydd agor dolen amherthynol wrth agor statws +- Dangos botym "Ychwanegu" yn y lle cywir pan nad oes unrhyw hidlyddion +- Cywirwyd chwalfa eraill diff --git a/fastlane/metadata/android/cy/changelogs/105.txt b/fastlane/metadata/android/cy/changelogs/105.txt new file mode 100644 index 0000000..ec39844 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/105.txt @@ -0,0 +1,11 @@ +Tusky 22.0 beta 3 + +Cywiriadau sydd yn ychwanegu: + +- Cywirwyd chwalfa wrth weld trywydd +- Cywirwyd chwalfa wrth brosesu hidlydd Mastodon +- Gallwch glicio dolennau ym mywgraffiadau hysbysiadau dilyn / ceisiadau i'ch dilyn +- Diweddariadau Hysbysiadau Android: + - Dylai hysbysiad Android ar gyfer hysbysiad Mastodon yn cael ei ddangos unwaith + - Mae hysbysiadau Android yn cael ei grwpio gan ddull hysbysiad Mastodon (dilyn, crybwyll, hybu, ac ati) + - Cywirwyd posibilrwydd hysbysiadau coll diff --git a/fastlane/metadata/android/cy/changelogs/106.txt b/fastlane/metadata/android/cy/changelogs/106.txt new file mode 100644 index 0000000..4ec9bc2 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/106.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 4 + +Cywiriadau: + +- Cywirwyd y nôl niferus o hysbysiadau os ydy'r ap yn cael ei ffurfweddu gyda chyfrifon amryfal diff --git a/fastlane/metadata/android/cy/changelogs/107.txt b/fastlane/metadata/android/cy/changelogs/107.txt new file mode 100644 index 0000000..8c4f8c5 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/107.txt @@ -0,0 +1,6 @@ +Tusky 22.0 beta 5 + +Cywiriadau: + +- Dychwelwyd llyfrgell APNG er mwyn cywiro emojis wedi'u hanimeiddio +- Cadw copi lleol y marciwr hysbysiad rhag ofn dyw'r gweinydd ddim yn cefnogi'r API diff --git a/fastlane/metadata/android/cy/changelogs/108.txt b/fastlane/metadata/android/cy/changelogs/108.txt new file mode 100644 index 0000000..a3ef0f8 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/108.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 6 + +Cywiriadau: + +- Cadw eich safle darllen yn y tab Hysbysiadau yn amlach diff --git a/fastlane/metadata/android/cy/changelogs/109.txt b/fastlane/metadata/android/cy/changelogs/109.txt new file mode 100644 index 0000000..ee7da72 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/109.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 7 + +Cywiriadau: + + +### Cywiriadau gwall pwysig + +- Wrth greu hysbysiadau Android, nôl pob hysbysiadau Mastodon dyledus +- Byddai clicio "Creu" o hysbysiad yn dewis y cyfrif anghywir +- Sicrhau mae'r "ID hysbysiad wedi'i ddarllen yn ddiweddaraf" yn cael ei gadw i'r cyfrif cywir diff --git a/fastlane/metadata/android/cy/changelogs/110.txt b/fastlane/metadata/android/cy/changelogs/110.txt new file mode 100644 index 0000000..5a85921 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/110.txt @@ -0,0 +1,20 @@ +Tusky 22.0 + +Nodweddion newydd: + +- Gwylio hashnodau tueddiadol +- Dilyn hashnodau newydd +- Gwell trefniadaeth wrth ddewis ieithoedd +- Dangos y gwahaniaeth rhwng fersiynau o bost +- Cefnogi hidlenni Mastodon v4 +- Opsiwn i ddangos ystadegau post yn y ffrwd +- A mwy... + +Gwelliannau: + +- Cofio'r tab a ddewiswyd a'r safle +- Cadw'r hysbysiadau tan eu ddarllen +- Dangos testun RTL a LTR yn gywir ym mhroffiliau +- Cywiro cyfrifiad hyd post +- Cyhoeddi disgrifiadau delwedd bob amser +- A mwy... diff --git a/fastlane/metadata/android/cy/changelogs/111.txt b/fastlane/metadata/android/cy/changelogs/111.txt new file mode 100644 index 0000000..6575dcb --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/111.txt @@ -0,0 +1,15 @@ +Tusky 23.0 beta 1 + +Nodwedd newydd: + +- Opsiwn newydd i newid testun y rhyngwyneb + +Cywiriadau: + +- Cadw gwybodaeth cyfrif yn gywir +- Hysbysiadau "tynnu" ar ddyfeisiau sydd yn defnyddio fersiwn Android <= 11 +- Gweithio o gwmpas gwall Android yn perthyn i'r gallu maes testun i "anghofio" y gallan nhw gopïo neu gludo +- Fydd weld gwahaniaethau yn yr hanes golygu ddim yn mynd y tu hwnt i ymyl y sgrin +- Fydd yr ap ddim yn chwalu os nad oes gan eich gweinydd unrhyw hanes golygu +- Ychwanegu botwm "Dileu" wrth olygu hidlydd +- Dangos emoji nad yw'n sgwâr yn gywir diff --git a/fastlane/metadata/android/cy/changelogs/112.txt b/fastlane/metadata/android/cy/changelogs/112.txt new file mode 100644 index 0000000..e714fea --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/112.txt @@ -0,0 +1,6 @@ +Tusky 23.0 beta 2 + +Cywiriadau: + +- Chwalu posibl wrth olygu meysydd proffil +- Dewislen cyd-destun rhy fawr wrth olygu disgrifiad delweddau diff --git a/fastlane/metadata/android/cy/changelogs/113.txt b/fastlane/metadata/android/cy/changelogs/113.txt new file mode 100644 index 0000000..2963afc --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/113.txt @@ -0,0 +1,15 @@ +Tusky 23.0 + +Nodweddion newydd: + +- Opsiwn newydd i newid maint testun y rhyngwyneb + +Cywiriadau: + +- Cadw gwybodaeth cyfrif yn gywir +- Hysbysiadau "tynnu" ar ddyfeisiau sydd yn defnyddio fersiwn Android <= 11 +- Gwall Android lle gall maes testun yn "anghofio" y gallan nhw gopïo neu gludo +- Fydd weld newidiadau yn yr hanes golygu ddim yn mynd y tu hwnt i ymyl y sgrin +- Fydd yr ap ddim yn chwalu os nad oes gan eich gweinydd unrhyw hanes golygu +- Dangos emoji nad yw'n sgwâr yn gywir +- Chwalu posibl wrth olygu meysydd proffil diff --git a/fastlane/metadata/android/cy/changelogs/115.txt b/fastlane/metadata/android/cy/changelogs/115.txt new file mode 100644 index 0000000..06054f8 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/115.txt @@ -0,0 +1,10 @@ +Tusky 24.0 + +- Mae dyfyniadau bloc a blociau cod mewn negeseuon yn edrych yn well. +- Mae'r hen ymarweddiad y tab hysbysiadau wedi'i adfer. +- Mae bathodynnau rôl nawr yn cael eu dangos ar broffiliau. +- Mae'r chwaraewr fideo wedi'i wella. Nawr galli di ddewis cyflymder chwarae. +- Mae opsiwn newydd i ddefnyddio'r thema ddu wrth ddilyn thema'r system. +- Mae golygfa newydd i weld negeseuon tueddiadol ar gael yn y ddewislen ac fel tab a gwsmereiddiwyd. + +A llawer mwy o welliannau a datrysiadau! diff --git a/fastlane/metadata/android/cy/changelogs/117.txt b/fastlane/metadata/android/cy/changelogs/117.txt new file mode 100644 index 0000000..a6f8f93 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/117.txt @@ -0,0 +1,7 @@ +Tusky 24.1 + +- Bydd y sgrin yn aros ymlaen eto tra bod fideo yn chwarae. +- Mae gollwng cof wedi cael ei gyweirio. Dylai hyn yn gwella sefydlogrwydd a pherfformiad. +- Mae emojis nawr yn cael ei rhifo'n gywir fel 1 nod wrth greu neges. +- Wedi cyweirio chwalfa pan ddetholwyd testun ar rai dyfeisiau. +- Bydd yr eiconau yn nhestunau help ffrydiau gwag yn cael eu halinio yn gywir bob tro. diff --git a/fastlane/metadata/android/cy/changelogs/119.txt b/fastlane/metadata/android/cy/changelogs/119.txt new file mode 100644 index 0000000..f990e9b --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/119.txt @@ -0,0 +1,8 @@ +Tusky 25 + +- Cynnal API cyfieithu Mastodon +- Dangos iaith y neges +- Gwella trawsnewidiadau'r sgrin +- Mae gosodiadau hidlo wedi cael eu symud i osodiadau'r cyfrif +- Mae ystadegau'r neges bob amser yn dangos yn yr un lle +- Llawer o wella wedi'u cudd diff --git a/fastlane/metadata/android/cy/changelogs/58.txt b/fastlane/metadata/android/cy/changelogs/58.txt new file mode 100644 index 0000000..f2f984b --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/58.txt @@ -0,0 +1,13 @@ +Tusky v6.0 + +- Mae hidlwyr ffrwd wedi symud i ddewisiadau'ch cyfrif a byddant yn cysoni â'r gweinydd +- Nawr gallwch chi gael hashnod wedi'i deilwra fel tab yn y prif ryngwyneb +- Bellach gellir golygu rhestrau +- Diogelwch: dileu cefnogaeth ar gyfer TLS 1.0 a TLS 1.1, ac ychwanegu cefnogaeth i TLS 1.3 ar Android 6+ +- Bydd yr olygfa gyfansoddi nawr yn awgrymu emojis personol wrth ddechrau teipio +- Gosodiad thema newydd "dilyn thema system" +- Gwell hygyrchedd y ffrwd +- Bydd Tusky nawr yn anwybyddu hysbysiadau anhysbys ac ni fyddant yn chwalu mwyach +- Lleoliad newydd: Nawr gallwch chi ddiystyru iaith y system a gosod iaith wahanol yn Tusky +- Cyfieithiadau newydd: Tsieceg ac Esperanto +- Llawer o welliannau ac atebion eraill diff --git a/fastlane/metadata/android/cy/changelogs/61.txt b/fastlane/metadata/android/cy/changelogs/61.txt new file mode 100644 index 0000000..d4ff9db --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Cefnogaeth ar gyfer arddangos polau, pleidleisio a hysbysiadau pleidleisio +- Botymau newydd i hidlo'r tab hysbysu ac i ddileu pob hysbysiad +- dileu ac ailddrafftio eich tŵtiau eich hun +- dangosydd newydd sy'n dangos a yw cyfrif yn bot ar y ddelwedd proffil (gellir ei ddiffodd yn y dewisiadau) +- Cyfieithiadau newydd: Norwyeg Bokmål a Slofeneg. diff --git a/fastlane/metadata/android/cy/changelogs/67.txt b/fastlane/metadata/android/cy/changelogs/67.txt new file mode 100644 index 0000000..56eb592 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Gallwch nawr greu Polau o Tusky +- Gwell chwiliad +- Opsiwn newydd yn dewisiadau'ch cyfrif i ehangu rhybuddion cynnwys bob amser +- Mae gan avatars yn y drôr llywio siâp sgwâr crwn bellach +- Mae bellach yn bosibl adrodd defnyddwyr hyd yn oed pan nad ydynt byth yn postio statws +- Bydd Tusky nawr yn gwrthod cysylltu dros gysylltiadau clir-destun ar Android 6+ +- Llawer o welliannau bach eraill ac atgyweiriadau i fygiau diff --git a/fastlane/metadata/android/cy/changelogs/68.txt b/fastlane/metadata/android/cy/changelogs/68.txt new file mode 100644 index 0000000..a337e6d --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Mae'r datganiad hwn yn sicrhau cydnawsedd â Mastodon 3 ac yn gwella perfformiad a sefydlogrwydd. diff --git a/fastlane/metadata/android/cy/changelogs/70.txt b/fastlane/metadata/android/cy/changelogs/70.txt new file mode 100644 index 0000000..b8845cd --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Gallwch nawr nodi statws tudalen a rhestru'ch nodau tudalen yn Tusky. +- Gallwch nawr amserlennu toots gyda Thusky. Sylwch fod yn rhaid i'r amser a ddewiswch fod o leiaf 5 munud yn y dyfodol. +- Gallwch nawr ychwanegu rhestrau at y brif sgrin. +- Gallwch nawr bostio atodiadau sain gyda Tusky. + +A llawer o welliannau bach eraill ac atgyweiriadau bygiau! diff --git a/fastlane/metadata/android/cy/changelogs/72.txt b/fastlane/metadata/android/cy/changelogs/72.txt new file mode 100644 index 0000000..a0f30c6 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Hysbysiadau am geisiadau dilynol newydd pan fydd eich cyfrif wedi'i gloi +- Nodweddion newydd y gellir eu toglo ar y sgrin Dewisiadau: + - analluogi llithro rhwng tabiau + - dangos ymgom cadarnhau cyn rhoi hwb i dant + - dangos rhagolygon cyswllt mewn llinellau amser +- Bellach gellir tawelu sgyrsiau +- Bydd canlyniadau pleidleisio nawr yn cael eu cyfrifo ar sail nifer y pleidleiswyr ac nid ar gyfanswm y pleidleisiau sy'n gwneud polau amlddewis yn haws i'w deall +- Llawer o atgyweiriadau, y rhan fwyaf ohonynt yn ymwneud â chyfansoddi toots +- Gwell cyfieithiadau diff --git a/fastlane/metadata/android/cy/changelogs/74.txt b/fastlane/metadata/android/cy/changelogs/74.txt new file mode 100644 index 0000000..48db535 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Gwell prif ryngwyneb - gallwch nawr symud y tabiau i'r gwaelod +- Wrth tewi defnyddiwr, gallwch nawr hefyd benderfynu a ydych am tewi eu hysbysiadau +- Gallwch nawr ddilyn cymaint o hashnodau ag y dymunwch mewn un tab hashnod unigol +- Gwella'r ffordd y mae disgrifiadau cyfryngau yn cael eu harddangos fel ei fod yn gweithio hyd yn oed ar gyfer disgrifiadau hir iawn + +Log newid llawn: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/cy/changelogs/77.txt b/fastlane/metadata/android/cy/changelogs/77.txt new file mode 100644 index 0000000..808dc51 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- cefnogaeth ar gyfer nodiadau proffil (nodwedd o Mastodon 3.2.0) +- cefnogaeth ar gyfer cyhoeddiadau gweinyddol (nodwedd o Mastodon 3.1.0) + +- bydd avatar y cyfrif a ddewiswyd gennych nawr yn cael ei ddangos yn y prif far offer +- bydd clicio ar yr enw arddangos mewn llinell amser nawr yn agor tudalen proffil y defnyddiwr hwnnw + +- llawer o atgyweiriadau i fygiau a mân welliannau +- gwell cyfieithiadau diff --git a/fastlane/metadata/android/cy/changelogs/80.txt b/fastlane/metadata/android/cy/changelogs/80.txt new file mode 100644 index 0000000..3a701fd --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Cael gwybod pan fydd defnyddiwr a ddilynir yn postio - cliciwch ar eicon y gloch ar eu proffil! (Nodwedd o Mastodon 3.3.0) +- Mae'r nodwedd ddrafft yn Tusky wedi'i hailgynllunio'n llwyr i fod yn gyflymach, yn haws ei defnyddio ac yn llai ddrwg. +- Mae modd lles newydd sy'n eich galluogi i gyfyngu ar rai nodweddion Tusky wedi'i ychwanegu. +- Gall Tusky nawr animeiddio emojis personol. +Log newid llawn: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/cy/changelogs/82.txt b/fastlane/metadata/android/cy/changelogs/82.txt new file mode 100644 index 0000000..6eae50b --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Mae ceisiadau dilyn bellach bob amser yn cael eu dangos yn y brif ddewislen. +- Mae gan y dewisiad amser ar gyfer amserlennu post ddyluniad sy'n gyson â gweddill yr app nawr +Log newid llawn: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/cy/changelogs/83.txt b/fastlane/metadata/android/cy/changelogs/83.txt new file mode 100644 index 0000000..d32fb4e --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Mae'r datganiad hwn yn trwsio damwain wrth roi capsiwn ar ddelweddau diff --git a/fastlane/metadata/android/cy/changelogs/87.txt b/fastlane/metadata/android/cy/changelogs/87.txt new file mode 100644 index 0000000..14e630a --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- Mae'r rhesymeg llwytho llinell amser wedi'i hailysgrifennu'n llwyr er mwyn bod yn gyflymach, yn llai ddrwg ac yn haws ei chynnal. +- Gall Tusky nawr animeiddio emojis wedi'u teilwra ar fformat APNG a WebP wedi'i hanimeiddio. +- Llawer o atgyweiriadau +- Cefnogaeth i Android 11 +- Cyfieithiadau newydd: Gaeleg yr Alban, Galiseg, Wcreineg +- Gwell cyfieithiadau diff --git a/fastlane/metadata/android/cy/changelogs/89.txt b/fastlane/metadata/android/cy/changelogs/89.txt new file mode 100644 index 0000000..b85fb8e --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- Mae "Ar agor fel ..." hefyd ar gael yn y ddewislen ar broffiliau cyfrifon wrth ddefnyddio cyfrifon lluosog +- Mae mewngofnodi bellach yn cael ei drin mewn WebView o fewn yr ap +- Cefnogaeth i Android 12 +- cefnogaeth i'r API cyfluniad enghraifft Mastodon newydd +- a llawer o atebion a gwelliannau bach eraill diff --git a/fastlane/metadata/android/cy/changelogs/91.txt b/fastlane/metadata/android/cy/changelogs/91.txt new file mode 100644 index 0000000..7398205 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Cefnogaeth ar gyfer mathau newydd o hysbysiadau Mastodon 3.5 +- Mae'r bathodyn bot bellach yn edrych yn well ac yn addasu i'r thema a ddewiswyd +- Bellach gellir dewis testun ar olwg manylion y post +- Trwsio llawer o fygiau, gan gynnwys un a oedd yn atal mewngofnodi ar Android 6 ac yn is diff --git a/fastlane/metadata/android/cy/changelogs/94.txt b/fastlane/metadata/android/cy/changelogs/94.txt new file mode 100644 index 0000000..abfc49f --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Cefnogaeth i Unified Push. I actifadu'r gefnogaeth bydd yn rhaid i chi ail-fewngofnodi i'ch cyfrifon. +- Mae nifer yr ymatebion i swydd bellach wedi'i nodi mewn llinellau amser. +- Bellach gellir tocio delweddau wrth gyfansoddi post. +- Mae proffiliau bellach yn dangos y dyddiad y cawsant eu creu. +- Wrth edrych ar restr mae'r teitl nawr yn cael ei arddangos yn y bar offer. +- Llawer o atgyweiriadau byg +- Gwelliannau cyfieithu diff --git a/fastlane/metadata/android/cy/changelogs/97.txt b/fastlane/metadata/android/cy/changelogs/97.txt new file mode 100644 index 0000000..92ae754 --- /dev/null +++ b/fastlane/metadata/android/cy/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- Eicon Ap Newydd gan Dzuk https://dzuk.zone/ +- Gallwch nawr ddilyn hashnodau. Cliciwch ar hashnod ac yna ar yr eicon yn y bar offer. +- Cefnogaeth i Android 13 +- cwymplen newydd yn y golwg cyfansoddi i osod iaith post +- Mae'r tab cyfryngau mewn proffiliau bellach yn parchu cyfryngau sensitif ac yn llwytho'n llyfnach. +- Mae bellach yn bosibl gosod pwynt ffocws delwedd cyn ei hanfon y post +- Opsiwn newydd i ddangos eich enw defnyddiwr llawn yn y bar offer diff --git a/fastlane/metadata/android/cy/full_description.txt b/fastlane/metadata/android/cy/full_description.txt new file mode 100644 index 0000000..d0f2aa5 --- /dev/null +++ b/fastlane/metadata/android/cy/full_description.txt @@ -0,0 +1,12 @@ +Mae Tusky yn gleient ysgafn i Mastodon, gweinydd rhwydwaith cymdeithasol cod-agored am ddim. + +• Material Design +• Gweithredwyd y rhan fwyaf o API Mastodon +• Cefnogaeth i gyfrifon lluosog +• Thema dywyll a golau gyda'r posibilrwydd newid yn awtomatig ar sail amser y dydd +• Drafftiau - cyfansoddi negeseuon a'u cadw yn nes ymlaen +• Dewis rhwng arddulliau emoji gwahanol +• Wedi'i optimeiddio ar gyfer pob maint sgrin +• Hollol cod-agored - dim dibyniaethau nad ydynt yn agored, fel gwasanaethau Google + +Er mwyn dysgu mwy am Mastodon, ewch i https://joinmastodon.org/ diff --git a/fastlane/metadata/android/cy/short_description.txt b/fastlane/metadata/android/cy/short_description.txt new file mode 100644 index 0000000..0882f46 --- /dev/null +++ b/fastlane/metadata/android/cy/short_description.txt @@ -0,0 +1 @@ +Cleient amlgyfrif ar gyfer y rhwydwaith cymdeithasol Mastodon diff --git a/fastlane/metadata/android/cy/title.txt b/fastlane/metadata/android/cy/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/cy/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/de/changelogs/100.txt b/fastlane/metadata/android/de/changelogs/100.txt new file mode 100644 index 0000000..677b061 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Beiträge können nun bearbeitet werden +- Neue Einstellungen für die bevorzugte Leserichtung +- Größere Medienvorschauen und ein neuer Hinweis, dass Medien eine Beschreibung haben +- Konten können nun direkt von der Profilansicht zu Listen hinzugefügt werden +und viele weitere Verbesserungen diff --git a/fastlane/metadata/android/de/changelogs/103.txt b/fastlane/metadata/android/de/changelogs/103.txt new file mode 100644 index 0000000..81d9f98 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/103.txt @@ -0,0 +1,16 @@ +Tusky 22.0 Beta 1 + +Neue Funktionen: +- Sieh dir angesagte Hashtags an +- Bearbeite Bildbeschreibungen und Fokuspunkte +- »Aktualisieren«-Menü für Barrierefreiheit +- Filter aus Mastodon V4 werden unterstützt +- Detaillierte Unterschiede beim Bearbeiten eines Beitrags +- Option zum Anzeigen von Beitragsstatistiken in der Timeline + +Behobene Fehler: +- Anzeigen von Bedienelemente bei der Audiowiedergabe +- Korrekte Berechnung der Beitragslänge +- Bildbeschreibungen werden immer veröffentlicht + +und mehr diff --git a/fastlane/metadata/android/de/changelogs/104.txt b/fastlane/metadata/android/de/changelogs/104.txt new file mode 100644 index 0000000..ca9b067 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/104.txt @@ -0,0 +1,9 @@ +Tusky 22.0 Beta 2 + +Behobene Fehler: +- Ladegeschwindigkeit von Benachrichtigungen verbessert +- 0/1/1+ werden bei Antworten wieder angezeigt +- Bei gefilterten Beiträgen Filtertitel anstatt Filterschlagwörter anzeigen +- Öffnen eines Beitrags öffnet keinen nicht zugehörigen Link mehr +- »Hinzufügen« wird nun korrekt angezeigt, wenn keine Filter vorhanden sind +- Diverse Abstürze behoben diff --git a/fastlane/metadata/android/de/changelogs/105.txt b/fastlane/metadata/android/de/changelogs/105.txt new file mode 100644 index 0000000..f318c06 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/105.txt @@ -0,0 +1,9 @@ +Tusky 22.0 Beta 3 + +Behobene Fehler: +- Abstürze beim Ansehen eines Threads und beim Verarbeiten von Mastodonfiltern behoben +- Links in »Über mich« bei Benachrichtigungen für Follower-Anfragen sind anklickbar +- Aktualisierte Android-Benachrichtigungen: + - Mastodon-Benachrichtigungen sollten nur noch einmal angezeigt werden + - Werden anhand des Typs von Mastodon-Benachrichtigungen gruppiert (Beitrag geteilt, erwähnt, usw.) + - Möglichkeit, Benachrichtigungen zu verpassen, wurde beseitigt diff --git a/fastlane/metadata/android/de/changelogs/106.txt b/fastlane/metadata/android/de/changelogs/106.txt new file mode 100644 index 0000000..60b8541 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/106.txt @@ -0,0 +1,4 @@ +Tusky 22.0 Beta 4 + +Behobener Fehler: +- Wiederholtes Abrufen von Benachrichtigungen bei Konfiguration mit mehreren Konten behoben diff --git a/fastlane/metadata/android/de/changelogs/107.txt b/fastlane/metadata/android/de/changelogs/107.txt new file mode 100644 index 0000000..3bda7de --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/107.txt @@ -0,0 +1,5 @@ +Tusky 22.0 Beta 5 + +Behobene Fehler: +- »APNG library« auf eine ältere Version zurückgestuft, damit animierte Emojis wieder animiert sind +- Eine Kopie der Benachrichtigungsmarkierung wird lokal gespeichert, falls der Server die entsprechende API nicht unterstützt diff --git a/fastlane/metadata/android/de/changelogs/108.txt b/fastlane/metadata/android/de/changelogs/108.txt new file mode 100644 index 0000000..2be6e79 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/108.txt @@ -0,0 +1,4 @@ +Tusky 22.0 Beta 6 + +Behobener Fehler: +- Leseposition wird im Tab »Benachrichtigung« häufiger gespeichert diff --git a/fastlane/metadata/android/de/changelogs/109.txt b/fastlane/metadata/android/de/changelogs/109.txt new file mode 100644 index 0000000..529a95d --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/109.txt @@ -0,0 +1,6 @@ +Tusky 22.0 Beta 7 + +Behobene Fehler: +- Alle ausstehenden Mastodon-Benachrichtigungen werden nun beim Erstellen von Android-Benachrichtigungen abgerufen +- Beim Anlicken auf »Beitrag erstellen« in einer Benachrichtigung wurde das falsche Konto ausgewählt +- Die »ID der zuletzt gelesenen Benachrichtigung« wird nun im richtigen Konto gespeichert diff --git a/fastlane/metadata/android/de/changelogs/110.txt b/fastlane/metadata/android/de/changelogs/110.txt new file mode 100644 index 0000000..ddab507 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/110.txt @@ -0,0 +1,16 @@ +Tusky 22.0 + +Neu: +- Sieh dir angesagte Hashtags an +- Bessere Sortierung bei der Sprachauswahl +- Unterschiede zwischen den Versionen eines Beitrags ansehen +- Unterstützung für Filter aus Mastodon V4 +- Option zum Anzeigen von Beitragsstatistiken in der Timeline +- Und mehr … + +Behobene Fehler: +- Ausgewählter Tab und Position wird gespeichert +- Benachrichtigungen bleiben bis zum Lesen erhalten +- Korrekte Berechnung der Beitragslänge +- Bildbeschreibungen werden immer veröffentlicht +- Und mehr … diff --git a/fastlane/metadata/android/de/changelogs/111.txt b/fastlane/metadata/android/de/changelogs/111.txt new file mode 100644 index 0000000..6489ee8 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/111.txt @@ -0,0 +1,13 @@ +Tusky 23.0 Beta 1 + +Neue Funktionen: +- Skalierung des Oberflächentextes + +Behoben: +- Kontoinformationen werden korrekt gespeichert +- »Herunterziehen«-Benachrichtigungen für Android-Geräte mit Version <= 11 +- Android-Fehler, bei dem ein Textfeld das Kopieren/Einfügen „vergessen“ konnte +- Absturz behoben, wenn der Server keinen Beitragsbearbeitungsverlauf hat +- »Löschen«-Schaltfläche beim Bearbeiten eines Filters hinzugefügt +- Nicht-quadratische Emojis werden korrekt dargestellt +- und mehr diff --git a/fastlane/metadata/android/de/changelogs/112.txt b/fastlane/metadata/android/de/changelogs/112.txt new file mode 100644 index 0000000..553f13b --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/112.txt @@ -0,0 +1,5 @@ +Tusky 23.0 Beta 2 + +Behoben: +- möglicher Absturz beim Ändern von Zusatzfeldern im Profil +- überdimensioniertes Kontextmenü, wenn die Bildbeschreibung bearbeitet wird diff --git a/fastlane/metadata/android/de/changelogs/113.txt b/fastlane/metadata/android/de/changelogs/113.txt new file mode 100644 index 0000000..c2913cf --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/113.txt @@ -0,0 +1,13 @@ +Tusky 23.0 + +Neue Funktionen: +- Skalierung des Oberflächentextes + +Behoben: +- Kontoinformationen werden korrekt gespeichert +- »Herunterziehen«-Benachrichtigungen für Android-Geräte mit Version <= 11 +- Android-Fehler, bei dem ein Textfeld das Kopieren/Einfügen „vergessen“ konnte +- Absturz behoben, wenn der Server keinen Beitragsbearbeitungsverlauf hat +- »Löschen«-Schaltfläche beim Bearbeiten eines Filters hinzugefügt +- Nicht-quadratische Emojis werden korrekt dargestellt +- und mehr diff --git a/fastlane/metadata/android/de/changelogs/115.txt b/fastlane/metadata/android/de/changelogs/115.txt new file mode 100644 index 0000000..2ba1aa1 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/115.txt @@ -0,0 +1,9 @@ +Tusky 24.0 + +- Blockzitate und Code-Blocks sehen ansprechender aus +- Alte Verhaltensweise vom Tab »Benachrichtigungen« wurde wiederhergestellt +- Abzeichen sind auf Profilen sichtbar +- Beim Videoplayer kann nun die Wiedergabegeschwindigkeit geändert werden +- Neue Design-Einstellung, um das schwarze Design zu verwenden, wenn das System-Design ausgewählt ist +- Neue Ansicht für beliebte Beiträge im Menü und benutzerdefinierten Tabs verfügbar +- Viele weitere Verbesserungen und Fehlerbehebungen! diff --git a/fastlane/metadata/android/de/changelogs/117.txt b/fastlane/metadata/android/de/changelogs/117.txt new file mode 100644 index 0000000..31c793a --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/117.txt @@ -0,0 +1,6 @@ +Tusky 24.1 +- Bildschirm schaltet sich beim Anschauen eines Videos nicht mehr aus. +- Ein Memory Leak wurde behoben. Dies sollte die Stabilität und Leistung verbessern. +- Emojis werden jetzt korrekt als 1 Zeichen gezählt, wenn ein Beitrag verfasst wird. +- Absturz auf einigen Geräten bei der Textauswahl behoben. +- Symbole in den Hilfetexten von leeren Timelines werden nun immer korrekt ausgerichtet. diff --git a/fastlane/metadata/android/de/changelogs/119.txt b/fastlane/metadata/android/de/changelogs/119.txt new file mode 100644 index 0000000..b04c717 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/119.txt @@ -0,0 +1,7 @@ +Tusky 25 +- Unterstützung für die Mastodon-Übersetzungs-API +- Sichtbarkeit der Beitragssprache +- Bildschirmübergänge verbessert +- Filter-Einstellungen in die Konto-Einstellungen verschoben +- Beitragsstatistiken haben eine feste Position +- Eine Menge Stabilitäts- und Leistungsverbesserungen diff --git a/fastlane/metadata/android/de/changelogs/58.txt b/fastlane/metadata/android/de/changelogs/58.txt new file mode 100644 index 0000000..fe86e15 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/58.txt @@ -0,0 +1,13 @@ +Tusky v6.0 + +- Timeline-Filter in Kontoeinstellungen verschoben; werden mit dem Server synchronisiert +- Eigener Tab für Hashtags +- Listen sind nun bearbeitbar +- TLS 1.0 und 1.1 entfernt, 1.3 für Android 6+ hinzugefügt +- Automatische Emoji-Vorschläge beim Tippen +- „Systemdesign verwenden“ hinzugefügt +- Verbesserte Barrierefreiheit +- Tusky ignoriert nun unbekannte Benachrichtigungen +- Neue Einstellung: Individuelle App-Sprache +- Neue Sprachen: Tschechisch und Esperanto +- Fehlerkorrekturen diff --git a/fastlane/metadata/android/de/changelogs/61.txt b/fastlane/metadata/android/de/changelogs/61.txt new file mode 100644 index 0000000..e6934ae --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Unterstützung für das Anzeigen von Umfragen, Abstimmungen und Umfragebenachrichtigungen +- Neue Schaltflächen, um den Benachrichtigungstab zu filtern und alle Benachrichtigungen zu löschen +- Lösche & erstelle deine Beiträge neu +- Es wird auf dem Profilbild angezeigt, ob es sich bei einem Konto um einen Bot handelt (kann in den Einstellungen ausgeschaltet werden) +- Neue Übersetzungen: Norwegisch Bokmål und Slovenisch. diff --git a/fastlane/metadata/android/de/changelogs/67.txt b/fastlane/metadata/android/de/changelogs/67.txt new file mode 100644 index 0000000..51226e2 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Du kannst jetzt Umfragen erstellen +- Die Suche wurde verbessert +- Neue Option in den Profileinstellungen, um Inhaltswarnungen immer auszuklappen +- Profilbilder im Hauptmenü haben jetzt abgerundete Ecken +- Es ist jetzt möglich, Profile ohne Beiträge zu melden +- Tusky wird auf Android 6+ nur noch sichere Netzwerkverbindungen nutzen +- Viele andere kleine Verbesserungen und Fehlerkorrekturen diff --git a/fastlane/metadata/android/de/changelogs/68.txt b/fastlane/metadata/android/de/changelogs/68.txt new file mode 100644 index 0000000..0921e0c --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Diese Veröffentlichung stellt die Kompatibilität mit Mastodon 3 sicher und verbessert die Geschwindigkeit und Stabilität der App. diff --git a/fastlane/metadata/android/de/changelogs/70.txt b/fastlane/metadata/android/de/changelogs/70.txt new file mode 100644 index 0000000..4c5f1fe --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Du kannst jetzt Lesezeichen hinzufügen und ansehen +- Du kannst jetzt Beiträge vorausplanen. Achtung, der geplante Zeitpunkt muss mindestens 5 Minuten in der Zukunft liegen! +- Du kannst jetzt Listen auf dem Hauptbildschirm anzeigen. +- Du kannst jetzt Audio-Anhänge versenden. + +Und viele andere Verbesserungen und Fehlerkorrekturen! diff --git a/fastlane/metadata/android/de/changelogs/72.txt b/fastlane/metadata/android/de/changelogs/72.txt new file mode 100644 index 0000000..fa8046d --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Benachrichtigungen über neue Follower-Anfragen, wenn das Konto privat ist +- Neue Funktionen, die aktiviert werden können: + - Wischgeste zum Wechseln zwischen Tabs + - Bestätigung vor dem Teilen eines Beitrags + - Linkvorschauen in Timelines +- Konversationen können jetzt stummgeschaltet werden +- Umfrageergebnisse für Umfragen mit Mehrfachauswahl sind jetzt einfacher zu verstehen +- Viele Fehlerkorrekturen, primär beim Verfassen von Beiträgen +- Verbesserte Übersetzungen diff --git a/fastlane/metadata/android/de/changelogs/74.txt b/fastlane/metadata/android/de/changelogs/74.txt new file mode 100644 index 0000000..709dbe5 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Verbessertes Interface — die Hauptnavigation kann jetzt auch unten angezeigt werden +- Entscheide beim Stummschalten eines Profils, ob du Benachrichtigungen stummschalten willst +- Es ist jetzt möglich, beliebig vielen Hashtags in einem Hashtag-Tab zu folgen +- Verbesserte Bildbeschreibungen, vorallem bei langen Texten + +Alle Änderungen: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/de/changelogs/77.txt b/fastlane/metadata/android/de/changelogs/77.txt new file mode 100644 index 0000000..2b76208 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- Unterstützung für Profilnotizen (Mastodon 3.2.0 Funktion) +- Unterstützung für Ankündigungen von Admins (Mastodon 3.1.0 Funktion) + +- Das Profilbild des ausgewählten Kontos wird nun in der Hauptnavigation angezeigt +- Ein Klick auf den Anzeigenamen in einer Timeline öffnet nun die Profilseite des jeweiligen Profils + +- Viele Fehlerkorrekturen und kleine Verbesserungen +- Verbesserte Übersetzungen diff --git a/fastlane/metadata/android/de/changelogs/80.txt b/fastlane/metadata/android/de/changelogs/80.txt new file mode 100644 index 0000000..18ec441 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Werde über neue Beiträge benachrichtigt — Klicke auf das Glockensymbol in Profilen! (Funktion von Mastodon 3.3.0) +- Die Entwurfsfunktion in Tusky wurde vollständig neu gestaltet, um schneller, nutzerfreundlicher und weniger fehleranfällig zu sein. +- Ein neuer Wohlbefinden-Modus, der dir erlaubt, bestimmte Funktionen von Tusky zu beschränken, wurde hinzugefügt. +- Tusky kann jetzt animierte Emojis darstellen. +Alle Änderungen: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/de/changelogs/82.txt b/fastlane/metadata/android/de/changelogs/82.txt new file mode 100644 index 0000000..8f11de0 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Follower-Anfragen werden jetzt immer im Menü angezeigt +- Der Zeitauswahldialog beim Planen eines Beitrags hat jetzt ein besseres Design +Alle Änderungen: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/de/changelogs/83.txt b/fastlane/metadata/android/de/changelogs/83.txt new file mode 100644 index 0000000..75ee4d4 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Diese Veröffentlichung behebt einen Absturz bei der Eingabe einer Bildbeschreibung diff --git a/fastlane/metadata/android/de/changelogs/87.txt b/fastlane/metadata/android/de/changelogs/87.txt new file mode 100644 index 0000000..b67110b --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- Die Logik des Ladens der Timeline wurde komplett neu geschrieben, um schneller, weniger fehlerhaft und wartungsfreundlicher zu sein. +- Tusky kann nun benutzerdefinierte Emojis im APNG- & Animated-WebP-Format darstellen. +- Viele Fehlerbehebungen +- Unterstützung von Android 11 +- Neue Übersetzungen: Schottisches Gälisch, Galicisch, Ukrainisch +- Verbesserte Übersetzungen diff --git a/fastlane/metadata/android/de/changelogs/89.txt b/fastlane/metadata/android/de/changelogs/89.txt new file mode 100644 index 0000000..3c9065d --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- „Als %s öffnen“ ist jetzt auch im Menü der Kontoprofile verfügbar, wenn mehrere Konten verwendet werden +- Die Anmeldung erfolgt nun über WebView innerhalb der App +- Unterstützung für Android 12 +- Unterstützung für die neue „Mastodon instance configuration API“ +- und einige andere kleine Fehlerbehebungen und Verbesserungen diff --git a/fastlane/metadata/android/de/changelogs/91.txt b/fastlane/metadata/android/de/changelogs/91.txt new file mode 100644 index 0000000..327f5b5 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Unterstützung für neue Benachrichtigungstypen aus Mastodon 3.5 +- Das Bot-Symbol sieht jetzt besser aus und passt sich dem gewählten App-Design an +- Der Text in den Beitragsdetails kann jetzt ausgewählt werden +- Viele Fehler behoben, inklusive einem, der Anmeldungen auf Android 6 und älter verhindert hat diff --git a/fastlane/metadata/android/de/changelogs/94.txt b/fastlane/metadata/android/de/changelogs/94.txt new file mode 100644 index 0000000..8db0c9a --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Push-Benachrichtigungen via Unified Push. Um Unified Push zu verwenden, musst du dich erneut anmelden. +- Die Anzahl an Antworten unter einem Beitrag wird jetzt in der Timeline angezeigt. +- Bilder können jetzt vor dem Veröffentlichen zugeschnitten werden. +- Das Erstellungsdatum eines Profils wird jetzt angezeigt. +- Beim Betrachten einer Liste ist jetzt der Listenname ersichtlich. +- Fehlerbehebungen +- verbesserte Übersetzungen diff --git a/fastlane/metadata/android/de/changelogs/97.txt b/fastlane/metadata/android/de/changelogs/97.txt new file mode 100644 index 0000000..ea7f8c9 --- /dev/null +++ b/fastlane/metadata/android/de/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- Neues Appsymbol von Dzuk https://dzuk.zone +- Du kannst Hashtags folgen. Tippe auf einen Hashtag und dann auf das Symbol in der Hauptnavigation +- Unterstützung für Android 13 +- Neues Auswahlmenü zum Festlegen der Beitragssprache +- Der Medien-Tab in Profilen respektiert nun Mediendateien mit Inhaltswarnung und lädt schneller +- Es ist nun möglich, den Fokuspunkt eines Bildes vor der Veröffentlichung festzulegen +- Der vollständige Profilname kann nun in der Hauptleiste angezeigt werden diff --git a/fastlane/metadata/android/de/full_description.txt b/fastlane/metadata/android/de/full_description.txt new file mode 100644 index 0000000..74c386b --- /dev/null +++ b/fastlane/metadata/android/de/full_description.txt @@ -0,0 +1,12 @@ +Tusky ist ein kompakter Client für Mastodon, ein freier und quelloffener Server für soziales Netzwerken. + +• Material Design +• Fast alle Mastodon-APIs implementiert +• Unterstützung mehrerer Konten +• Dunkles und helles Design mit der Möglichkeit, es automatisch je nach Tageszeit wechseln zu lassen +• Entwürfe — schreibe Beiträge und speichere sie für später +• Auswahl zwischen verschiedenen Emoji-Stilen +• Optimiert für alle Bildschirmgrößen +• Komplett quelloffenen - keine unfreien Abhängigkeiten z. B. von Google-Diensten + +Um mehr über Mastodon zu erfahren, besuche https://joinmastodon.org/ diff --git a/fastlane/metadata/android/de/short_description.txt b/fastlane/metadata/android/de/short_description.txt new file mode 100644 index 0000000..e21665c --- /dev/null +++ b/fastlane/metadata/android/de/short_description.txt @@ -0,0 +1 @@ +Ein Client für das soziale Netzwerk Mastodon, der mehrere Konten unterstützt diff --git a/fastlane/metadata/android/de/title.txt b/fastlane/metadata/android/de/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/de/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/el/changelogs/100.txt b/fastlane/metadata/android/el/changelogs/100.txt new file mode 100644 index 0000000..37b1d5c --- /dev/null +++ b/fastlane/metadata/android/el/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Υποστήριξη για επεξεργασία αναρτήσεων +- Νέες ρυθμίσεις για έλεγχο των προτιμητέων διευθύνσεων ανάγνωσης +- Προεπισκόπηση μεγαλύτερων πολυμέσων και νέα εμφάνιση για την αναγνώριση των πολυμέσων με περιγραφή +- Πλέον είναι δυνατή η εισαγωγή λογαριασμών σε λίστες από τα προφιλ τους +και πολλά περισσότερα diff --git a/fastlane/metadata/android/el/full_description.txt b/fastlane/metadata/android/el/full_description.txt new file mode 100644 index 0000000..71bd11e --- /dev/null +++ b/fastlane/metadata/android/el/full_description.txt @@ -0,0 +1,12 @@ +Το Tusky είνια μια ελαφριά εφαρμογή για το Mastodon, ένα δωρεάν και ανοικτού κώδικα διακομιστή κοινωνικού δικτύου. + +• Material Design +• Τα περισσότερα API του Mastodon είναι προσβάσιμα +• Υποστήριξη πολλαπλών λογαριασμών +• Σκούρο και φωτεινό θέμα με δυνατότητα αυτόματης εναλλαγής ανάλογα την ώρα της ημέρας +• Πρόχειρα - γράψτε toots και αποθηκεύστε τα για αργότερα +• Επιλέξτε μεταξύ διαφορετικών στιλ emoji +• Προσαρμόζεται σε όλα τα μεγέθη οθόνης +• Απόλυτα ανοικτού-κώδια - χωρίς μη-ελεύθερες εξαρτήσεις όπως υπηρεσίες της Google + +Για να μάθαιτε περισσότερα για το Mastodon, επισκεφτείτε το https://joinmastodon.org/ diff --git a/fastlane/metadata/android/el/short_description.txt b/fastlane/metadata/android/el/short_description.txt new file mode 100644 index 0000000..a32eb46 --- /dev/null +++ b/fastlane/metadata/android/el/short_description.txt @@ -0,0 +1 @@ +Μια εφαρμογή πολλαπλών λογαριασμών του κοινωνικού δικτύου Mastodon diff --git a/fastlane/metadata/android/el/title.txt b/fastlane/metadata/android/el/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/el/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/en-US/changelogs/100.txt b/fastlane/metadata/android/en-US/changelogs/100.txt new file mode 100644 index 0000000..d2c4504 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Support for post editing +- New setting to control preferred reading direction +- Larger media previews and a new overlay to indicate media with description +- It is now possible to add accounts to lists from their profile +and much more \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/103.txt b/fastlane/metadata/android/en-US/changelogs/103.txt new file mode 100644 index 0000000..896b871 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/103.txt @@ -0,0 +1,18 @@ +Tusky 22.0 beta 1 + +Features including: + +- View trending hashtags +- Edit image descriptions and focus point +- "Refresh" menu for accessibility +- Support Mastodon v4 filters +- Show detailed differences when a post is edited +- Option to show post statistics in the timeline + +Fixes including: + +- Show player controls during audio playback +- Correct post length calculation +- Always publish image captions + +and much more diff --git a/fastlane/metadata/android/en-US/changelogs/104.txt b/fastlane/metadata/android/en-US/changelogs/104.txt new file mode 100644 index 0000000..dffabc4 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/104.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 2 + +Fixes including: + +- Improved notification loading speed +- Restore showing 0/1/1+ for replies +- Show filter titles, not filter keywords, on filtered posts +- Fixed a bug where opening a status could open an unrelated link +- Show "Add" button in correct place when there are no filters +- Fixed assorted crashes diff --git a/fastlane/metadata/android/en-US/changelogs/105.txt b/fastlane/metadata/android/en-US/changelogs/105.txt new file mode 100644 index 0000000..0dc1147 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/105.txt @@ -0,0 +1,11 @@ +Tusky 22.0 beta 3 + +Fixes including: + +- Fixed crash when viewing a thread +- Fixed crash processing Mastodon filters +- Links in bios of follow/follow request notifications are clickable +- Android Notifications updates + - Android notification for a Mastodon notification should only be shown once + - Android notifications are grouped by Mastodon notification type (follow, mention, boost, etc) + - Potential for missing notifications has been removed diff --git a/fastlane/metadata/android/en-US/changelogs/106.txt b/fastlane/metadata/android/en-US/changelogs/106.txt new file mode 100644 index 0000000..28785ce --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/106.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 4 + +Fixes: + +- Fixed repeated fetch of notifications if configured with multiple accounts diff --git a/fastlane/metadata/android/en-US/changelogs/107.txt b/fastlane/metadata/android/en-US/changelogs/107.txt new file mode 100644 index 0000000..a194ca7 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/107.txt @@ -0,0 +1,7 @@ +Tusky 22.0 beta 5 + +Fixes: + +- Rolled back APNG library to fix broken animated emojis +- Save local copy of notification marker in case server does not support the API + diff --git a/fastlane/metadata/android/en-US/changelogs/108.txt b/fastlane/metadata/android/en-US/changelogs/108.txt new file mode 100644 index 0000000..f4e1b0c --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/108.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 6 + +Fixes: + +- Save reading position in the Notifications tab more frequently diff --git a/fastlane/metadata/android/en-US/changelogs/109.txt b/fastlane/metadata/android/en-US/changelogs/109.txt new file mode 100644 index 0000000..6f21acc --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/109.txt @@ -0,0 +1,11 @@ +Tusky 22.0 beta 7 + +Fixes: + + +### Significant bug fixes + +- Fetch all outstanding Mastodon notifications when creating Android notifications +- Clicking "Compose" from a notification would set the wrong account +- Ensure "last read notification ID" is saved to the correct account + diff --git a/fastlane/metadata/android/en-US/changelogs/110.txt b/fastlane/metadata/android/en-US/changelogs/110.txt new file mode 100644 index 0000000..199381a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/110.txt @@ -0,0 +1,20 @@ +Tusky 22.0 + +New features: + +- View trending hashtags +- Follow new hashtags +- Better ordering when selecting languages +- Show the difference between versions of a post +- Support Mastodon v4 filters +- Option to show post statistics in the timeline +- And more... + +Fixes: + +- Remember selected tab and position +- Keep notifications until read +- Correctly display mixed RTL and LTR text in profiles +- Correct post length calculation +- Always publish image captions +- And more... diff --git a/fastlane/metadata/android/en-US/changelogs/111.txt b/fastlane/metadata/android/en-US/changelogs/111.txt new file mode 100644 index 0000000..8b3d203 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/111.txt @@ -0,0 +1,15 @@ +Tusky 23.0 beta 1 + +New features: + +- New preference to scale UI text + +Fixes: + +- Save account information correctly +- "pull" notifications on devices running Android versions <= 11 +- Work around Android bug where text fields could "forget" they can copy/paste +- Viewing "diffs" in edit history will not extend off screen edge +- Don't crash if your server has no post edit history +- Add a "Delete" button when editing a filter +- Show non-square emoji correctly diff --git a/fastlane/metadata/android/en-US/changelogs/112.txt b/fastlane/metadata/android/en-US/changelogs/112.txt new file mode 100644 index 0000000..883e352 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/112.txt @@ -0,0 +1,6 @@ +Tusky 23.0 beta 2 + +Fixes: + +- Potential crash when editing profile fields +- Oversized context menu when editing image descriptions diff --git a/fastlane/metadata/android/en-US/changelogs/113.txt b/fastlane/metadata/android/en-US/changelogs/113.txt new file mode 100644 index 0000000..53665b6 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/113.txt @@ -0,0 +1,15 @@ +Tusky 23.0 + +New features: + +- New preference to scale UI text + +Fixes: + +- Save account information correctly +- "pull" notifications on devices running Android versions <= 11 +- Android bug where text fields could "forget" they can copy/paste +- Viewing changes in edit history will not extend off screen edge +- Don't crash if your server has no post edit history +- Show non-square emoji correctly +- Potential crash when editing profile fields diff --git a/fastlane/metadata/android/en-US/changelogs/115.txt b/fastlane/metadata/android/en-US/changelogs/115.txt new file mode 100644 index 0000000..82ab623 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/115.txt @@ -0,0 +1,10 @@ +Tusky 24.0 + +- Blockquotes and code blocks in posts now look nicer. +- The old behavior of the notification tab has been restored. +- Role badges are now shown on profiles. +- The video player has been improved. You can now select the playback speed. +- New theme option to use the black theme when following the system design. +- A new view to see trending posts is available both in the menu and as custom tab. + +And a lot of other improvements and fixes! diff --git a/fastlane/metadata/android/en-US/changelogs/117.txt b/fastlane/metadata/android/en-US/changelogs/117.txt new file mode 100644 index 0000000..e386487 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/117.txt @@ -0,0 +1,7 @@ +Tusky 24.1 + +- The screen will stay on again while a video is playing. +- A memory leak has been fixed. This should improve stability and performance. +- Emojis are now correctly counted as 1 character when composing a post. +- Fixed a crash when text was selected on some devices. +- The icons in the help texts of empty timelines will now always be correctly aligned. diff --git a/fastlane/metadata/android/en-US/changelogs/119.txt b/fastlane/metadata/android/en-US/changelogs/119.txt new file mode 100644 index 0000000..102d507 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/119.txt @@ -0,0 +1,8 @@ +Tusky 25 + +- Support Mastodon translation API +- Show post language +- Improved screen transitions +- Filter settings are moved to account preferences +- Post stats now have stable position +- A lot off under-the-hood stability & performance improvements diff --git a/fastlane/metadata/android/en-US/changelogs/58.txt b/fastlane/metadata/android/en-US/changelogs/58.txt new file mode 100644 index 0000000..59111c2 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/58.txt @@ -0,0 +1,13 @@ +Tusky v6.0 + +- Timeline filters have moved to Account Preferences and will sync with the server +- You can now have a custom hashtag as tab in the main interface +- Lists can now be edited +- Security: removed support for TLS 1.0 and TLS 1.1, and added support for TLS 1.3 on Android 6+ +- The compose view will now suggest custom emojis when starting to type +- New theme setting "follow system theme" +- Improved timeline accessibility +- Tusky will now ignore unknown notifications and no longer crash +- New setting: You can now override the system language and set a different language in Tusky +- New translations: Czech and Esperanto +- A lot of other improvements and fixes diff --git a/fastlane/metadata/android/en-US/changelogs/61.txt b/fastlane/metadata/android/en-US/changelogs/61.txt new file mode 100644 index 0000000..f138a64 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Support for displaying polls, voting and poll notifications +- New buttons to filter the notification tab and to delete all notifications +- delete & redraft your own toots +- new indicator that shows if an account is a bot on the profile image (can be turned off in the preferences) +- New translations: Norwegian Bokmål and Slovenian. \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/67.txt b/fastlane/metadata/android/en-US/changelogs/67.txt new file mode 100644 index 0000000..4a130c0 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- You can now create Polls from Tusky +- Improved search +- New option in Account Preferences to always expand content warnings +- Avatars in the navigation drawer have now a rounded square shape +- It is now possible to report users even when they never posted a status +- Tusky will now refuse to connect over cleartext connections on Android 6+ +- A lot of other small improvements and bug fixes diff --git a/fastlane/metadata/android/en-US/changelogs/68.txt b/fastlane/metadata/android/en-US/changelogs/68.txt new file mode 100644 index 0000000..61b9e75 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +This release ensures compatibility with Mastodon 3 and improves performance and stability. diff --git a/fastlane/metadata/android/en-US/changelogs/70.txt b/fastlane/metadata/android/en-US/changelogs/70.txt new file mode 100644 index 0000000..1242668 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- You can now bookmark statuses & list your bookmarks in Tusky. +- You can now schedule toots with Tusky. Note that the time you select has to be at least 5 minutes in the future. +- You can now add lists to the main screen. +- You can now post audio attachments with Tusky. + +And a lot of other small improvements and bug fixes! \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/72.txt b/fastlane/metadata/android/en-US/changelogs/72.txt new file mode 100644 index 0000000..0ba9c63 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Notifications about new follow requests when your account is locked +- New features that can be toggled on the Preferences screen: + - disable swiping between tabs + - show a confirmation dialog before boosting a toot + - show link previews in timelines +- Conversations can now be muted +- Poll results will now be calculated based on the number of voters and not on the number of total votes which makes multichoice polls easier to understand +- A lot of bugfixes, most of them related to composing toots +- Improved translations diff --git a/fastlane/metadata/android/en-US/changelogs/74.txt b/fastlane/metadata/android/en-US/changelogs/74.txt new file mode 100644 index 0000000..1b09378 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Improved main interface - you can now move the tabs to the bottom +- When muting a user, you can now also decide whether to mute their notifications +- You can now follow as many hashtags as you want in one single hashtag tab +- Improved the way media descriptions are displayed so it works even for super long descriptions + +Full changelog: https://github.com/tuskyapp/Tusky/releases \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/77.txt b/fastlane/metadata/android/en-US/changelogs/77.txt new file mode 100644 index 0000000..0a31ea3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- support for profile notes (Mastodon 3.2.0 feature) +- support for admin announcements (Mastodon 3.1.0 feature) + +- the avatar of your selected account will now be shown in the main toolbar +- clicking the display name in a timeline will now open the profile page of that user + +- a lot of bug fixes and small improvements +- improved translations \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/80.txt b/fastlane/metadata/android/en-US/changelogs/80.txt new file mode 100644 index 0000000..14d28f0 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Get notified when a followed user posts - click the bell icon on their profile! (Mastodon 3.3.0 feature) +- The draft feature in Tusky has been completely redesigned to be faster, more user friendly and less buggy. +- A new wellbeing mode that allows you to limit certain Tusky features has been added. +- Tusky can now animate custom emojis. +Full changelog: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/en-US/changelogs/82.txt b/fastlane/metadata/android/en-US/changelogs/82.txt new file mode 100644 index 0000000..6e6afdd --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Follow requests are now always shown in the main menu. +- The time picker for scheduling a post has a design consistent with the rest of the app now +Full changelog: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/en-US/changelogs/83.txt b/fastlane/metadata/android/en-US/changelogs/83.txt new file mode 100644 index 0000000..5eb77c2 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +This release fixes a crash when captioning images diff --git a/fastlane/metadata/android/en-US/changelogs/87.txt b/fastlane/metadata/android/en-US/changelogs/87.txt new file mode 100644 index 0000000..81411b4 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- The timeline loading logic has been completely rewritten in order to be faster, less buggy and easier to maintain. +- Tusky can now animate custom emojis in APNG & Animated WebP format. +- A lot of bugfixes +- Support for Android 11 +- New translations: Scottish Gaelic, Galician, Ukrainian +- Improved translations diff --git a/fastlane/metadata/android/en-US/changelogs/89.txt b/fastlane/metadata/android/en-US/changelogs/89.txt new file mode 100644 index 0000000..4b666c3 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Open as..." is now also available in the menu on account profiles when using multiple accounts +- Login is now handled in a WebView within the app +- Support for Android 12 +- support for the new Mastodon instance configuration API +- and a lot of other small fixes and improvements diff --git a/fastlane/metadata/android/en-US/changelogs/91.txt b/fastlane/metadata/android/en-US/changelogs/91.txt new file mode 100644 index 0000000..e1d9830 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Support for new Mastodon 3.5 notification types +- The bot badge now looks better and adjusts to the selected theme +- Text can now be selected on the post detail view +- Fixed a lot of bugs, including one that prevented logins on Android 6 and lower diff --git a/fastlane/metadata/android/en-US/changelogs/94.txt b/fastlane/metadata/android/en-US/changelogs/94.txt new file mode 100644 index 0000000..8dc15d8 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Support for Unified Push. To activate the support you will have to relogin into your accounts. +- The number of responses to a post is now indicated in timelines. +- Images can now by cropped while composing a post. +- Profiles now show the date when they were created. +- When viewing a list the title is now displayed in the toolbar. +- A lot of bugfixes +- Translation improvements \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/changelogs/97.txt b/fastlane/metadata/android/en-US/changelogs/97.txt new file mode 100644 index 0000000..146cf24 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- New App icon by Dzuk https://dzuk.zone/ +- You can now follow hashtags. Click on a hashtag and then on the icon in the toolbar. +- Support for Android 13 +- new dropdown in the compose view to set the language of a post +- The media tab in profiles now respects sensitive media and loads smoother. +- It is now possible to set the focus point of an image before posting +- New option to show your full username in the toolbar \ No newline at end of file diff --git a/fastlane/metadata/android/en-US/full_description.txt b/fastlane/metadata/android/en-US/full_description.txt new file mode 100644 index 0000000..6d6383a --- /dev/null +++ b/fastlane/metadata/android/en-US/full_description.txt @@ -0,0 +1,12 @@ +Tusky is a lightweight client for Mastodon, a free and open-source social network server. + +• Material Design +• Most Mastodon APIs implemented +• Multi-Account support +• Dark and light theme with the possibility to auto-switch based on the time of day +• Drafts - compose toots and save them for later +• Choose between different emoji styles +• Optimized for all screen sizes +• Completely open-source - no non-free dependencies like Google services + +To learn more about Mastodon, visit https://joinmastodon.org/ diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png new file mode 100644 index 0000000..a8256b1 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png new file mode 100644 index 0000000..8879519 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/icon.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/00_login.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/00_login.png new file mode 100644 index 0000000..22074d1 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/00_login.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/01_timeline.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/01_timeline.png new file mode 100644 index 0000000..88d9e74 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/01_timeline.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/02_compose.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/02_compose.png new file mode 100644 index 0000000..2a098a1 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/02_compose.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/03_profile.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/03_profile.png new file mode 100644 index 0000000..68df57f Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/03_profile.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/04_favourites.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/04_favourites.png new file mode 100644 index 0000000..78dee66 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/04_favourites.png differ diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/05_reply.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/05_reply.png new file mode 100644 index 0000000..695bd81 Binary files /dev/null and b/fastlane/metadata/android/en-US/images/phoneScreenshots/05_reply.png differ diff --git a/fastlane/metadata/android/en-US/short_description.txt b/fastlane/metadata/android/en-US/short_description.txt new file mode 100644 index 0000000..61ce2bd --- /dev/null +++ b/fastlane/metadata/android/en-US/short_description.txt @@ -0,0 +1 @@ +A multi account client for the social network Mastodon diff --git a/fastlane/metadata/android/en-US/title.txt b/fastlane/metadata/android/en-US/title.txt new file mode 100644 index 0000000..d541745 --- /dev/null +++ b/fastlane/metadata/android/en-US/title.txt @@ -0,0 +1 @@ +Tusky \ No newline at end of file diff --git a/fastlane/metadata/android/eo/full_description.txt b/fastlane/metadata/android/eo/full_description.txt new file mode 100644 index 0000000..54185ea --- /dev/null +++ b/fastlane/metadata/android/eo/full_description.txt @@ -0,0 +1,12 @@ +Tusky estas malpeza klienta aplikaĵo por Mastodon, libera kaj malfermitkoda socireta servilo. + +• „Material Design“ +• Funkcias kun la plejmulto de API de Mastodon +• Subtenas uzadon de pluraj uzantkontoj +• Hela kaj malhela etosoj kun la eblo de aŭtomata ŝanĝo inter ili depende de la momento de la tago +• Malnetoj: ekredaktu hupojn kaj konservu ilin por poste +• Elektu inter malsamaj emoĝi-stiloj +• Optimumigita por ĉiuj ekrangrandoj +• Tute malfermitkoda: sen ajna dependo de fermitkodaj servoj kiel tiuj de Google + +Por ekscii pli pri Mastodon, vizitu https://joinmastodon.org/ diff --git a/fastlane/metadata/android/eo/short_description.txt b/fastlane/metadata/android/eo/short_description.txt new file mode 100644 index 0000000..88d8f8a --- /dev/null +++ b/fastlane/metadata/android/eo/short_description.txt @@ -0,0 +1 @@ +Pluruzanta aplikaĵo por la socia reto Mastodon diff --git a/fastlane/metadata/android/eo/title.txt b/fastlane/metadata/android/eo/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/eo/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/es/changelogs/100.txt b/fastlane/metadata/android/es/changelogs/100.txt new file mode 100644 index 0000000..795614b --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Soporte para edición de publicaciones +- Nueva configuración para controlar la dirección de lectura preferida +- Vistas previas de medios más grandes y una nueva superposición para indicar medios con descripción +- Ahora es posible agregar cuentas a listas desde su perfil +y mucho más diff --git a/fastlane/metadata/android/es/changelogs/103.txt b/fastlane/metadata/android/es/changelogs/103.txt new file mode 100644 index 0000000..77d1308 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/103.txt @@ -0,0 +1,18 @@ +Tusky 22.0 beta 1 + +Entre sus características se incluyen: + +- Visualización de etiquetas que son tendencia +- Editar descripciones de imágenes y puntos de enfoque +- Menú "Refrescar" Para fines de accesibilidad +- Soporte para filtros de Mastodon v4 +- Mostrar información detallada cuando se edita una publicación +- Opción para mostrar estadísticas de publicaciones en la cronología + +Entre las correcciones se incluye: + +- Mostrar controles del reproductor durante la reproducción de audio +- Calculación correcta de la longitud de una publicación +- Siempre publicar descripciones de imágenes + +Entre otras diff --git a/fastlane/metadata/android/es/changelogs/58.txt b/fastlane/metadata/android/es/changelogs/58.txt new file mode 100644 index 0000000..4fe2e38 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/58.txt @@ -0,0 +1,10 @@ +Tusky v6.0 + +- Filtros de linea de tiempo movidos a preferencias de cuenta y se sincroniza con servidor +- Hashtags personalizados como una pestaña en la interfaz principal +- Puedes editar listas +- Seguridad: el soporte para TLS 1.0 y 1.1 se ha removido, se añade soporte para TLS 1.3 en Android 6+ +- La vista de composición ahora sugiere emojis personalizados al escribir +- Nueva opción para seguir tema del sistema +- Nueva traducción: Czech y Esperanto +- Otras mejoras y arreglos diff --git a/fastlane/metadata/android/es/changelogs/61.txt b/fastlane/metadata/android/es/changelogs/61.txt new file mode 100644 index 0000000..abdb1ca --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Soporte para mostrar encuestas, votar y recibir notificaciones de ellas +- Nuevos botones para filtrar la pestaña de notificaciones y borrar todas las notificaciones +- Borra y edita tus propios toots +- nuevo indicador para mostrar si la cuenta es un bot en la foto de perfil (puede apagarse en las preferencias) +- Nueva traduccion: Norwegian Bokmål and Slovenian. diff --git a/fastlane/metadata/android/es/changelogs/67.txt b/fastlane/metadata/android/es/changelogs/67.txt new file mode 100644 index 0000000..0348538 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Ahora puedes crear encuestas desde Tusky +- Se mejoro la búsqueda +- Nueva opción en preferencias de Cuenta para siempre expandir contenido con aviso +- Los avatar en el cajón de navegación ahora tienen forma de cuadro redondeado +- Ahora puedes reportar usuarios incluso cuando nunca hayan tenido un estado +- Tusky no se conectara a conexiones cleartext en Android 6+ +- Otras mejoras y arreglo de errores diff --git a/fastlane/metadata/android/es/changelogs/68.txt b/fastlane/metadata/android/es/changelogs/68.txt new file mode 100644 index 0000000..8d7c5e6 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Esta versión asegura la compatibilidad con Mastodon 3 y mejora le estabilidad y desempeño. diff --git a/fastlane/metadata/android/es/changelogs/70.txt b/fastlane/metadata/android/es/changelogs/70.txt new file mode 100644 index 0000000..d9caf02 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Ahora puedes marcar estas y listar tus marcadores en Tusky. +- Ahora puedes programar toots con Tusky. Ten en cuenta que el tiempo que elijas tiene que ser al menos 5 minutos en el futuro. +- Ahora puedes añadir listas a la pantalla principal. +- Ahora puedes publicar audio adjunto en Tusky. + +¡Y muchos otros pequeños cambios y mejoras! diff --git a/fastlane/metadata/android/es/changelogs/72.txt b/fastlane/metadata/android/es/changelogs/72.txt new file mode 100644 index 0000000..f56782d --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/72.txt @@ -0,0 +1,9 @@ +Tusky v11.0 + +- Notificaciones sobre nuevas solicitudes de seguimiento cuando su cuenta está bloqueada +- Nuevas funciones que se pueden alternar en la pantalla de Preferencias: + - deshabilitar deslizar entre pestañas + - muestra un diálogo de confirmación antes de dar un toque + - Mostrar vistas previas de enlaces en líneas de tiempo +- Las conversaciones ahora se pueden silenciar +- Los resultados de la encuesta ahora se calcularán en función de la cantidad de votantes y no de la cantidad t diff --git a/fastlane/metadata/android/es/changelogs/74.txt b/fastlane/metadata/android/es/changelogs/74.txt new file mode 100644 index 0000000..23cdd96 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- La interfaz principal fue mejorada - puedes mover las pestañas a la parte inferior +- Al silenciar un usuario, puedes decidir silenciar sus notificaciones +- Ahora puedes seguir todos los hashtags que quieras en una sola pestaña de hashtags +- Se mejoro la forma en la que la descripción de medios se muestra, ahora funciona para descripciones mas largas + +Todos los cambios: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/es/changelogs/77.txt b/fastlane/metadata/android/es/changelogs/77.txt new file mode 100644 index 0000000..c70aa23 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- soporte para notas de perfil (Mastodon 3.2.0) +- soporte para anuncios de administradores (Mastodon 3.1.0) + +- el avatar de tu cuenta escogida ahora se muestra en la barra de herramientas principal +- al presionar el nombre de usuario en la linea de tiempo se abrira el perfil de dicho usuario + +- arreglo de errores y algunas mejoras +- traducciones mejoradas diff --git a/fastlane/metadata/android/es/changelogs/80.txt b/fastlane/metadata/android/es/changelogs/80.txt new file mode 100644 index 0000000..3579d86 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Recibe notificaciones cuando un usuario que sigues hace una publicación - toca la campana en su perfil! (Mastodon 3.3.0) +- La función de borradores en Tusky ha sido completamente rediseñada para ser mas rápida, amigable y con menos errores +- Se añadió un nuevo modo de bienestar digital que puede limitar la funcionalidad de Tusky +- Tusky ahora puede animar emojis personalizados. +Todos los cambios: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/es/changelogs/82.txt b/fastlane/metadata/android/es/changelogs/82.txt new file mode 100644 index 0000000..3baaa41 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Las solicitudes de seguimiento ahora se muestran siempre en el menú principal +- El selector de tiempo para programar una publicación tiene un diseño consistente con el resto de la app +Registro completo: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/es/changelogs/83.txt b/fastlane/metadata/android/es/changelogs/83.txt new file mode 100644 index 0000000..0acbe89 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Esta versión arregla un fallo al subtitular imágenes diff --git a/fastlane/metadata/android/es/changelogs/87.txt b/fastlane/metadata/android/es/changelogs/87.txt new file mode 100644 index 0000000..4dd7494 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v6.0 + +- La lógica de carga de linea de tiempo fue reescrita para ser más rápida, tener menos bugs, y ser más fácil de mantener. +- Tusky puede animar emojis personalizados en formatos APNG y Animated WebP +- Muchos arreglos de bugs +- Soporte para Android 11 +- Nuevas traducciones: Gaélico escocés, Galiciano y Ucraniano +- Traducciones mejoradas diff --git a/fastlane/metadata/android/es/changelogs/89.txt b/fastlane/metadata/android/es/changelogs/89.txt new file mode 100644 index 0000000..6d004d9 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky versión 17.0 + +-"Abrir como..." ahora disponible en el menú del perfil cuando se usan varias cuentas. +- Para el registro ahora se utiliza WebView dentro de la app +- Soporte para Android 12 +- Soporte para la nueva API de configuración de instancias de Mastodon +- y muchas otras mejoras y correcciones diff --git a/fastlane/metadata/android/es/changelogs/91.txt b/fastlane/metadata/android/es/changelogs/91.txt new file mode 100644 index 0000000..2478c76 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky versión 18.0 + +- Soporte para las nuevas notificaciones de Mastodon 3.5 +- El símbolo de bot ahora se adecúa mejor al tema seleccionado +- Ahora se puede seleccionar el texto de una publicación en la vista detallada +- Correcciones de muchos errores, incluyendo el que impedía registros en Android 6 y anteriores diff --git a/fastlane/metadata/android/es/changelogs/94.txt b/fastlane/metadata/android/es/changelogs/94.txt new file mode 100644 index 0000000..8a59f9d --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky versión 19.0 + +- Soporte para Unified Push. Para activar el soporte tendrás que volver a iniciar sesión en tus cuentas. +- Ahora se muestra el número de respuestas a una publicación en las cronologías. +- Ahora se pueden recortar imágenes cuando se escribe una publicación. +- Ahora se muestra la fecha en la que se crearon los perfiles. +- Cuando se ve una lista, ahora se muestra el nombre en la barra de herramientas. +- Correcciones de diversos errores +- Mejoras en las traducciones diff --git a/fastlane/metadata/android/es/changelogs/97.txt b/fastlane/metadata/android/es/changelogs/97.txt new file mode 100644 index 0000000..09c9851 --- /dev/null +++ b/fastlane/metadata/android/es/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky versión 20.0 + +- Nuevo icono de la aplicación, de Dzuk https://dzuk.zone/ +- Ahora se pueden seguir etiquetas. Clica en una etiqueta y después en el icono de la barra. +- Soporte para Android 13 +- Se puede seleccionar el idioma en el que se escribe una publicación +- La pestaña de multimedia en los perfiles ahora respeta el contenido sensible y carga mejor. +- Ahora es posible fijar el foco de una imagen antes de publicarla +- Nueva opción para mostrar el nombre de usuario completo en la barra diff --git a/fastlane/metadata/android/es/full_description.txt b/fastlane/metadata/android/es/full_description.txt new file mode 100644 index 0000000..ab3edf8 --- /dev/null +++ b/fastlane/metadata/android/es/full_description.txt @@ -0,0 +1,12 @@ +Tusky es un cliente ligero para Mastodon, un servidor de red social gratuito y de código abierto. + +• Material Design +• La mayoría de las API de Mastodon están implementadas +• Soporte de múltiples cuentas +• Tema oscuro y claro con la posibilidad de cambiar automáticamente según la hora del día +• Borradores - crear los toots y guárdalos para más adelante. +• Elige entre diferentes estilos de emojis +• Optimizado para todos los tamaños de pantalla. +• Completamente de código abierto: sin dependencias no libres como los servicios de Google + +Para obtener más información sobre Mastodon, visite https://joinmastodon.org/ diff --git a/fastlane/metadata/android/es/short_description.txt b/fastlane/metadata/android/es/short_description.txt new file mode 100644 index 0000000..88fb958 --- /dev/null +++ b/fastlane/metadata/android/es/short_description.txt @@ -0,0 +1 @@ +Un cliente multicuentas para la red social Mastodon diff --git a/fastlane/metadata/android/es/title.txt b/fastlane/metadata/android/es/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/es/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/eu/changelogs/58.txt b/fastlane/metadata/android/eu/changelogs/58.txt new file mode 100644 index 0000000..5e5cb67 --- /dev/null +++ b/fastlane/metadata/android/eu/changelogs/58.txt @@ -0,0 +1,9 @@ +Tusky v6.0 + +- Kronikaren iragazkiak Kontuen Hobespenetara aldatu dira eta zerbitzariarekin sinkronizatuko dira +- Orain interfaze nagusian traola pertsonalizatu bat izan dezakezu +- Zerrendak horain editatu daitezke +- Segurtasuna: TLS 1.0 eta TLS 1.1 sistemetarako laguntza kendu eta TLS 1.3-ren laguntza gehitu da Android 6+ bertsioan +- Konposatutako ikuspegiak emoji pertsonalizatuak proposatzen ditu idazten hastean +- Ezarpen berria "jarraitu sistemaren gaia" +- Kronologiaren irisgarritasuna hobetu diff --git a/fastlane/metadata/android/eu/changelogs/61.txt b/fastlane/metadata/android/eu/changelogs/61.txt new file mode 100644 index 0000000..4881066 --- /dev/null +++ b/fastlane/metadata/android/eu/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Inkestak, botoak eta inkestak jakinarazteko laguntza +- Botoi berriak jakinarazpen fitxa iragazteko eta jakinarazpen guztiak ezabatzeko +- Ezabatu eta berriro diseinatu zure toot-ak +- Profil irudian kontua bot-a den erakusten duen adierazle berria (lehentasunetan desaktibatu daiteke) +- Itzulpen berriak: Bokmål Norvegiera eta Esloveniera. diff --git a/fastlane/metadata/android/eu/changelogs/67.txt b/fastlane/metadata/android/eu/changelogs/67.txt new file mode 100644 index 0000000..4eb0df3 --- /dev/null +++ b/fastlane/metadata/android/eu/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Orain Tusky-tik inkestak sor ditzakezu +- Bilaketa hobetua +- Aukera berria Kontuen Hobespenetan beti edukien abisuak zabaltzeko +- Nabigazioko tiraderaren avatarrek forma karratu biribildua dute orain +- Horain posible da erabiltzaileen berri ematea inoiz egoera bat argitaratu ez dutenean ere +- Tusky-k orain ukatu egingo du Android 6+ bertsioetako testu-konexioen bidez konektatzea +- Beste hainbat hobekuntza txiki eta akats konponketa diff --git a/fastlane/metadata/android/eu/changelogs/68.txt b/fastlane/metadata/android/eu/changelogs/68.txt new file mode 100644 index 0000000..7296a77 --- /dev/null +++ b/fastlane/metadata/android/eu/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Argitalpen honek Mastodon 3-rekin bateragarritasuna bermatzen du eta errendimendua eta egonkortasuna hobetzen ditu. diff --git a/fastlane/metadata/android/eu/changelogs/70.txt b/fastlane/metadata/android/eu/changelogs/70.txt new file mode 100644 index 0000000..d257e21 --- /dev/null +++ b/fastlane/metadata/android/eu/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Orain tooten laster-markak egin ditzakezu eta zure laster-markak Tuskyn zerrendatu ditzakezu. +- Orain tootak programa ditzakezu Tuskyrekin. Kontuan izan hautatzen duzun denborak gutxienez 5 minutu izan behar duela etorkizunean. +- Orain zerrendak gehi ditzakezu pantaila nagusian. +- Orain audioak argitara ditzakezu Tuskyrekin. + +Eta beste hainbat hobekuntza txiki eta akatsen zuzenketa! diff --git a/fastlane/metadata/android/eu/changelogs/72.txt b/fastlane/metadata/android/eu/changelogs/72.txt new file mode 100644 index 0000000..f249229 --- /dev/null +++ b/fastlane/metadata/android/eu/changelogs/72.txt @@ -0,0 +1,9 @@ +Tusky v11.0 + +- Jarraipena egiteko eskaera berriei buruzko jakinarazpenak zure kontua blokeatuta dagoenean. +- Hobespenen pantailan txanda daitezkeen eginbide berriak: + - Desgaitu fitxen artean irristatzea. + - Erakutsi baieztapen elkarrizketa-koadroa tuta bultzatu aurretik. + - Erakutsi esteken aurrebistak kronogrametan. +- Elkarrizketak isil daitezke. +- Inkesten emaitzak orain hautesle kopuruaren arabera kalkulatuko dira horrek egiten du anitz aukerako inkesta errazago ulertzen. diff --git a/fastlane/metadata/android/eu/changelogs/74.txt b/fastlane/metadata/android/eu/changelogs/74.txt new file mode 100644 index 0000000..f084cb0 --- /dev/null +++ b/fastlane/metadata/android/eu/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Interfaze nagusia hobetuta - fitxak beheko aldera eraman ditzakezu. +- Erabiltzailea mututzean, orain ere jakinarazpenak isilarazi nahi dituzun erabaki dezakezu. +- Orain nahi dituzun traola traola fitxa bakarrean jarrai ditzakezu. +- Multimedia deskribapenak bistaratzeko modua hobetu da, beraz deskribapen oso luzeetarako ere funtzionatzen du. + +Aldaketa erregistro osoa: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/eu/changelogs/77.txt b/fastlane/metadata/android/eu/changelogs/77.txt new file mode 100644 index 0000000..b00469f --- /dev/null +++ b/fastlane/metadata/android/eu/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- Profileko oharren laguntza. (Mastodon 3.2.0-ren ezaugarria) +- Administratzaileentzako iragarkien laguntza. (Mastodon 3.1.0-ren ezaugarria) + +- Hautatutako kontuaren abatarra tresna barra nagusian erakutsiko da. +- Denbora-lerro batean bistaratzeko izena klikatuz gero, erabiltzaile horren profil orria irekiko da. + +- Akats konponketa asko eta hobekuntza txikiak. +- Itzulpenak hobetuta. diff --git a/fastlane/metadata/android/eu/changelogs/80.txt b/fastlane/metadata/android/eu/changelogs/80.txt new file mode 100644 index 0000000..aefcc4e --- /dev/null +++ b/fastlane/metadata/android/eu/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Jaso jakinarazpena jarraitutako erabiltzaile batek argitaratzen duenean - egin klik bere profileko kanpaiaren ikonoan! (Mastodon 3.3.0-ren ezaugarria) +- Tusky-ren zirriborroa erabat birmoldatu da azkarragoa, erabilerrazagoa eta txikiagoa izan dadin. +- Tusky-ren zenbait ezaugarri mugatzea ahalbidetzen duen ongizate modu berria gehitu da. +- Tusky-k emoji pertsonalizatuak animatu ditzake orain. +Aldaketa erregistro osoa : https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/eu/changelogs/82.txt b/fastlane/metadata/android/eu/changelogs/82.txt new file mode 100644 index 0000000..2d58b9e --- /dev/null +++ b/fastlane/metadata/android/eu/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Jarraitzeko eskaerak menu nagusian agertzen dira beti. +- Mezu bat antolatzeko denbora-hautatzaileak orain gainerako aplikazioekin bat datorren diseinua du +Aldaketa erregistro osoa: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/eu/changelogs/83.txt b/fastlane/metadata/android/eu/changelogs/83.txt new file mode 100644 index 0000000..454e2e5 --- /dev/null +++ b/fastlane/metadata/android/eu/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Bertsio honek konpondu egin du irudiak azpititulatzean huts egitea diff --git a/fastlane/metadata/android/eu/changelogs/87.txt b/fastlane/metadata/android/eu/changelogs/87.txt new file mode 100644 index 0000000..fbe0eea --- /dev/null +++ b/fastlane/metadata/android/eu/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky 16.0 bertsioa + +- Denbora-lerroaren karga-logika guztiz berridatzia izan da, azkarragoa eta mantentzeko errazagoa izateko, baita akats gutxiago eduki dezan ere. +- Orain, Tusky aplikazioak APNG eta animaziodun WebP formatudun emoji pertsonalizatuak anima ditzake. +- Akats-zuzenketa ugari +- Android 11rekin bateragarria +- Itzulpen berriak: Eskoziako gaelera, galiziera eta ukrainera +- Hobetutako itzulpenak diff --git a/fastlane/metadata/android/eu/changelogs/97.txt b/fastlane/metadata/android/eu/changelogs/97.txt new file mode 100644 index 0000000..ae439e0 --- /dev/null +++ b/fastlane/metadata/android/eu/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- Aplikaziorako ikono berria Dzuk-en eskutik https://dzuk.zone +- Orain traolak jarrai ditzakezu. Traolan klik egin eta ondoren, tresna-barrako ikonoa. +- Android 13rekin bateragarria +- Bidalketa bat egiterakoan, erabilitako hizkuntza aukera dezakezu +- Profiletako edukien fitxak orain eduki hunkigarria errespetatzen du eta arinago kargatzen da. +- Orain, irudiaren foku-puntua bidali aurretik zehazteko aukera duzu +- Tresna-barran aukera berri bat zure erabiltzaile-izen osoa erakusteko diff --git a/fastlane/metadata/android/eu/full_description.txt b/fastlane/metadata/android/eu/full_description.txt new file mode 100644 index 0000000..2b0899c --- /dev/null +++ b/fastlane/metadata/android/eu/full_description.txt @@ -0,0 +1,12 @@ +Tusky bezero arina da Mastodonentzat, doako eta irekia den sare sozialen zerbitzarirako. + +• Material Design +• Mastodon API gehienak inplementatu dira +• Kontu anitzeko laguntza +• Gai iluna eta argia eguneko orduaren arabera automatikoki aldatzeko aukerarekin +• Zirriborroak - idatzi tootak eta gorde itzazu gerorako +• Aukeratu emoji estilo desberdinen artean +• Pantaila tamaina guztietarako optimizatua +• Iturri guztiz irekiak - Google zerbitzuak bezalako doako menpekotasunik ez + +Mastodoni buruz gehiago jakiteko, bisitatu https://joinmastodon.org/ diff --git a/fastlane/metadata/android/eu/short_description.txt b/fastlane/metadata/android/eu/short_description.txt new file mode 100644 index 0000000..021722f --- /dev/null +++ b/fastlane/metadata/android/eu/short_description.txt @@ -0,0 +1 @@ +Mastodon sare sozialerako kontu anitzeko bezero bat diff --git a/fastlane/metadata/android/eu/title.txt b/fastlane/metadata/android/eu/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/eu/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/fa/changelogs/100.txt b/fastlane/metadata/android/fa/changelogs/100.txt new file mode 100644 index 0000000..03156c4 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/100.txt @@ -0,0 +1,7 @@ +تاسکی ۲۱٫۰ + +- پشتیبانی از ویرایش فرسته +- تنظیمات جدید برای واپایش جهت خوانش ترجیحی +- پیشنمایشهای رسانهٔ بزرگتر و نشانگر روکار جدید برای رسانههای دارای شرح +- اکنون افزودن حسابها به فهرستها از نمایهشان ممکن است +و بسیاری چیزهای بیشتر diff --git a/fastlane/metadata/android/fa/changelogs/103.txt b/fastlane/metadata/android/fa/changelogs/103.txt new file mode 100644 index 0000000..18f5d8e --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/103.txt @@ -0,0 +1,18 @@ +تاسکی ۲۲٫۰ بتا ۱ + +ویژگیها شامل: + +- دیدن برچسبهای داغ +- ویرایش شرح تصویر و نقطهٔ تمرکز +- فهرست «بازخوانی» برای دسترسیپذیری +- پشیبانی پالایههای ماستودون ن۴ +- نمایش تفاوتهای جزیی ویرایش فرسته +- گزینه برای نمایش آمار فرستهها در خط زمانی + +رفع اشکالها شامل: + +- نمایش واپایشگرهای پخشکننده در طول پخش صدا +- تصحیح محاسبهٔ طول فرسته +- انتشار همیشگی عنوان تصویرها + +و بیشتر diff --git a/fastlane/metadata/android/fa/changelogs/104.txt b/fastlane/metadata/android/fa/changelogs/104.txt new file mode 100644 index 0000000..968b7a7 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/104.txt @@ -0,0 +1,10 @@ +تاسکی ۲۲٫۰ بتا ۲ + +تغییرها شامل: + +- بهبود سرعت بار کردن آگاهی +- بازگردانی نمایش ۰/۱/۱+ برای پاسخها +- نمایش عنوان پالایهها و نه کلیدواژههای پالایه روی فرستههای پالوده +- رفع اشکالی که گشودن یک وضعیت میتوانست پیوند نامرتبطی را بگشاید +- نمایش دکمهٔ «افزودن» در جای درست هنگام نبودن هیچ پالایهای +- رفع فروپاشیهای متفرّقه diff --git a/fastlane/metadata/android/fa/changelogs/105.txt b/fastlane/metadata/android/fa/changelogs/105.txt new file mode 100644 index 0000000..011da32 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/105.txt @@ -0,0 +1,11 @@ +تاسکی ۲۲٫۰ بتا ۳ + +تعمیرها شامل: + +- تعمیر فروپاشی هنگام دیدن رشته +- تعمیر فروپاشی در پردازش پالایههای ماستودون +- پیوندها در شرححالهای آگاهیهای پیگیریها و درخواستهایشان قابل کلیکند +- بهروز رسانیهای آگاهیهای اندروید + - آگاهی اندروید برای آگاهی ماستودون باید فقط یک بار نشان داده شود + - آگاهیهای اندروید بر اساس گونهٔ آگاهی ماستودون (پیگیری، نامبری، تقویت و…) گروه میشوند + - امکان از دست رفتن آگاهی برداشته شده diff --git a/fastlane/metadata/android/fa/changelogs/106.txt b/fastlane/metadata/android/fa/changelogs/106.txt new file mode 100644 index 0000000..34a3c6c --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/106.txt @@ -0,0 +1,5 @@ +تاسکی ۲۲٫۰ بتا ۴ + +رفع اشکالها: + +- تعمیر واکشی مکرّر آگاهیها در صورت پیکربندی بت چندین حساب diff --git a/fastlane/metadata/android/fa/changelogs/107.txt b/fastlane/metadata/android/fa/changelogs/107.txt new file mode 100644 index 0000000..98001e1 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/107.txt @@ -0,0 +1,6 @@ +تاسکی ۲۲٫۰ بتا ۵ + +رفع اشکالها: + +- برگرداندن کتابخانهٔ APNG برای تعمیر اموجیهای پویا +- ذخیرهٔ رونوشت محلی از علامتزن آگاهی در صورتی که کارساز از میانا پشتیبانی نکند diff --git a/fastlane/metadata/android/fa/changelogs/108.txt b/fastlane/metadata/android/fa/changelogs/108.txt new file mode 100644 index 0000000..a00ceeb --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/108.txt @@ -0,0 +1,5 @@ +تاسکی ۲۲٫۰ بتا ۶ + +رفع اشکالها: + +- ذخیرهٔ زودبهزودتر مکان خواندن در زبانهٔ آگاهیها diff --git a/fastlane/metadata/android/fa/changelogs/109.txt b/fastlane/metadata/android/fa/changelogs/109.txt new file mode 100644 index 0000000..1021c4f --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/109.txt @@ -0,0 +1,10 @@ +تاسکی ۲۲٫۰ بتا ۷ + +رفع اشکال: + + +## رفع اشکالهای برجسته + +- گرفتن تمامیآگاهیهای ماستودون مانده هنگام ساخت آگاهی اندروید +- کلیکروی ایجاد از آگاهی حساب اشتباهی را تنظیم میکرد +- اطمینان از ذخیرهٔ «آخرین شناسهٔ آگاهی خوانده شده» برای حساب درست diff --git a/fastlane/metadata/android/fa/changelogs/110.txt b/fastlane/metadata/android/fa/changelogs/110.txt new file mode 100644 index 0000000..8a372a5 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/110.txt @@ -0,0 +1,20 @@ +تاسکی ۲۲٫۰ + +ویژگیهای جدید: + +- دیدن برچسبهای داغ +- پیگیری برچسبهای جدید +- چینش بهتر هنگام گزینش زبانها +- نمایش تفاوت بین نگارشهای فرسته +- پشیبانی پالایههای ماستودون ن۴ +- گزینه برای نمایش آمار فرستهها در خط زمانی +- و بیشتر… + +رفع اشکالها: + +- به یاد سپردن مکان و زبانهٔ گزیده +- نگه داشتن آگاهیها تا زمان خوانده شدن +- نمایش درست متن دوجهته در نمایهها +- تصحیح محاسبهٔ طول فرسته +- نشر همیشگی متن تصویر +- و بیشتر… diff --git a/fastlane/metadata/android/fa/changelogs/111.txt b/fastlane/metadata/android/fa/changelogs/111.txt new file mode 100644 index 0000000..4b708c3 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/111.txt @@ -0,0 +1,15 @@ +تاسکی ۲۳٫۰ بتا + +ویژگیهای جدید: + +- ترجیح جدید برای مقیاس متن میانای کاربری + +رفع اشکالها: + +- ذخیرهٔ درست اطّلاعات حساب +- «گرفتن» آگاهیها روی افزارههای با نگارش اندروید کمتر از ۱۱ +- دور زدن مشکل امکان فراموشی توانایی رونوشت و جایگذاری در زمینههای متنی اندروید +- تجاوز نکردن از لیهٔ صفحه هنگام دیدن «تفاوتها» در تاریخچهٔ ویرایش +- فرونپاشیدن در صورت نبودن تاریخچهٔ ویرایش فرسته روی کارساز +- افزودن دکمهٔ «حذف» هنگام ویرایش یک پالایه +- نمایش درست اموجیهای نامربّعی diff --git a/fastlane/metadata/android/fa/changelogs/112.txt b/fastlane/metadata/android/fa/changelogs/112.txt new file mode 100644 index 0000000..2fc162f --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/112.txt @@ -0,0 +1,6 @@ +تاسکی ۲۳٫۰ بتا ۲ + +رفع اشکالها: + +- فروپاشی بالقوه هنگام ویرایش زمینههای نمایه +- فهرست بافتار بزرگ هنگام ویرایش شرح تصاویر diff --git a/fastlane/metadata/android/fa/changelogs/113.txt b/fastlane/metadata/android/fa/changelogs/113.txt new file mode 100644 index 0000000..2db0e7c --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/113.txt @@ -0,0 +1,15 @@ +تاسکی ۲۳٫۰ + +ویژگیهای جدید: + +- ترجیح جدید برای مقیاس متن میانای کاربری + +رفع اشکالها: + +- ذخیرهٔ درست اطّلاعات حساب +- «گرفتن» آگاهیها روی افزارههای با نگارش اندروید کمتر از ۱۱ +- مشکل امکان فراموشی توانایی رونوشت و جایگذاری در زمینههای متنی اندروید +- تجاوز نکردن از لیهٔ صفحه هنگام دیدن «تفاوتها» در تاریخچهٔ ویرایش +- فرونپاشیدن در صورت نبودن تاریخچهٔ ویرایش فرسته روی کارساز +- نمایش درست اموجیهای نامربّعی +- فروپاشی احتمالی هنگام ویرایش زمینههای نمایه diff --git a/fastlane/metadata/android/fa/changelogs/115.txt b/fastlane/metadata/android/fa/changelogs/115.txt new file mode 100644 index 0000000..0edd4b1 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/115.txt @@ -0,0 +1,10 @@ +تاسکی ۲۴٫۰ + +- نمای بهتر نقل قولها و بلوکهای کد در فرستهها. +- بازگشت رفتار قدیمی زبانهٔ آگاهیها. +- نمایش نشانهای نقش روی نمایهها. +- بهبود پخشکنندهٔ ویدیو. امکان گزینش سرعت پخش. +- گزینهٔ استفاده از زمینهٔ سیاه هنگام پیروی از طرّاحی سامانه. +- نمای جدید دیدن فرستههای داغ در فهرست و زبانهٔ سفارشی. + +و بسیاری بهبودها و رفع اشکالهای دیگر! diff --git a/fastlane/metadata/android/fa/changelogs/117.txt b/fastlane/metadata/android/fa/changelogs/117.txt new file mode 100644 index 0000000..97c9d61 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/117.txt @@ -0,0 +1,7 @@ +تاسکی ۲۴٫۱ + +- صفحه هنگامنمایش ویدیو روشن میماند. +- نشت حافظهای رفع شد. بهبود پایداری و کارایی. +- شکلکها هنگام ایجاد فرسته ۱ فرسته شمرده میشوند. +- رفع فروپاشی هنگام گزینش متن روی برخی افزارهها. +- نقشکهای متنهای راهنما در خطها زمانی خالی درست همتراز میشوند. diff --git a/fastlane/metadata/android/fa/changelogs/119.txt b/fastlane/metadata/android/fa/changelogs/119.txt new file mode 100644 index 0000000..63804cd --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/119.txt @@ -0,0 +1,8 @@ +تاسکی ۲۵ + +- پشتیبانی از میانای ترجمهٔ ماستودون +- نمایش زبان فرسته +- بهبود جابهجایی صفحهها +- جابهجایی رشتههای پالایه به ترجیحات حساب +- ثابت شدن موقعیت آمار فرستهها +- بسیاری از بهبودهای کارایی و پایداری diff --git a/fastlane/metadata/android/fa/changelogs/58.txt b/fastlane/metadata/android/fa/changelogs/58.txt new file mode 100644 index 0000000..257bbae --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/58.txt @@ -0,0 +1,13 @@ +تاسکی نگارش ۶٫۰ + +- انتقال پالایههای خطزمانی به ترجیحات حساب و همگام سازی با کارخواه +- میتوانید برچسب سفارشی را به عنوان زبانهای در رابط اصلی داشته باشید +- سیاههها میتوانند ویرایش شوند +- حذف پشتیبانی TLS ۱٫۰ و ۱٫۱ و افزودن پیشتیبانی برای TLS ۱٫۳ روی اندروید ۶ به بالا +- پیشنهاد شکلکهای سفارشی با نوشتن در نمای ایجاد +- سبک جدید «پیروی از سامانه» +- بهبود دسترسیپذیری خطزمانی +- نادیده گرفتن آگاهیهای ناشناخته +- میتوانید زبان سامانه را پایمال کنید +- ترجمههای چکو اسپرانتو +- بهبودهای بیشتر diff --git a/fastlane/metadata/android/fa/changelogs/61.txt b/fastlane/metadata/android/fa/changelogs/61.txt new file mode 100644 index 0000000..462443a --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/61.txt @@ -0,0 +1,7 @@ +تاسکی نگارش ۷٫۰ + +- پشتیبانی از نمایش نظرسنجیها، رأیها و آگاهیهای مربوط به نظرسنجی +- دکمهٔ جدید برای پالایش زبانهٔ آگاهی و حذف همهٔ آگاهیها +- حذف و بازنویسی بوقهای خودتان +- نشانگر جدید روی تصویر نمایه که نشان میدهد یک حساب، بات است (قابل خاموش کردن در ترجیحات) +- ترجمههای جدید: نروژی و اسلونیایی. diff --git a/fastlane/metadata/android/fa/changelogs/67.txt b/fastlane/metadata/android/fa/changelogs/67.txt new file mode 100644 index 0000000..f724022 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/67.txt @@ -0,0 +1,9 @@ +تاسکی نگارش ۹٫۰ + +- اکنون میتوانید از تاسکی، نظرسنجی ایجاد کنید +- جستوجوی بهبودیافته +- گزینهٔ جدید در ترجیحات حساب برای گسترش همیشگی هشدارهای محتوا +- آواتارها در کشوی ناوبری اکنون شکل مربّعی با لبههای گرد دارند +- اکنون گزارش کاربرانی که هیچ بوقی ندارند نیز ممکن است +- تاسکی اکنون روی اندروید ۶ به بالا از اتّصال غیر ایمن سر باز میزند +- بسیاری از بهبودها و رفع اشکالهای دیگر diff --git a/fastlane/metadata/android/fa/changelogs/68.txt b/fastlane/metadata/android/fa/changelogs/68.txt new file mode 100644 index 0000000..e2710e7 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/68.txt @@ -0,0 +1,3 @@ +تاسکی نگارش ۹٫۱ + +این نگارش، سازگاری با ماستودون ۳ را تأمین کرده و عملکرد و پایداری را بهبود میدهد. diff --git a/fastlane/metadata/android/fa/changelogs/70.txt b/fastlane/metadata/android/fa/changelogs/70.txt new file mode 100644 index 0000000..15f7b31 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/70.txt @@ -0,0 +1,8 @@ +تاسکی نگارش ۱۰٫۰ + +- اکنون میتوانید بوقها را نشان کرده و نشانکهایتان را فهرست کنید. +- اکنون میتوانید بوقها را زمانبندی کنید. توجّه داشته باشید که زمان گزیدهتان باید لااقل ۵ دقیقه بعد باشد. +- اکنون میتوانید فهرستها را به صفحهٔ اصلی بیفزایید. +- اکنون میتوانید پیوستهای صوتی بفرستید. + +و بسیاری از بهبودها و رفع اشکالهای ریز! diff --git a/fastlane/metadata/android/fa/changelogs/72.txt b/fastlane/metadata/android/fa/changelogs/72.txt new file mode 100644 index 0000000..40069d7 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/72.txt @@ -0,0 +1,11 @@ +تاسکی نگارش ۱۱٫۰ + +- آگاهیها دربارهٔ درخواستهای دنبال کردن جدید، هنگام قفل بودن حسابتان +- ویژگیهای جدیدی که وضعیتشان میتواند در صفحهٔ ترجیحات تغییر کند: + - از کار انداختن جابهجایی بین زبانهها با کشیدن + - نمایش یک گفتوگوی تأیید، پیش از تقویت یک بوق + - نمایش پیشنمایشهای پیوند در خط زمانی +- امکان خموش کردن گفتوگوها +- محاسبهٔ نتایج نظر سنجیهای چندگزینهای بر اساس تعداد رأیدهندگان و نه تعداد کل رأیها +- یک عالمه رفع اشکال که بیشترشان مربوط به ایجاد بوقهاست +- بهبود ترجمهها diff --git a/fastlane/metadata/android/fa/changelogs/74.txt b/fastlane/metadata/android/fa/changelogs/74.txt new file mode 100644 index 0000000..bafc87e --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/74.txt @@ -0,0 +1,8 @@ +تاسکی نگارش ۱۲٫۰ + +- واسط اصلی بهبودیافته - اکنون میتوانید زبانهها را به پایین ببرید +- هنگام خموش کردن کاربران، میتوانید آگاهیهایشان را هم خموش کنید +- اکنون میتوانید هرتعداد هشتگ که میخواهید را در یک زبانهٔ هشتگ دنبال کنید +- بهبود شیوهٔ نمایش توضیح رسانهها که برای توضیحات خیلی طولانی هم نشان داده شوند +… +گزارش دگرگونی کامل: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/fa/changelogs/77.txt b/fastlane/metadata/android/fa/changelogs/77.txt new file mode 100644 index 0000000..27f9a71 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/77.txt @@ -0,0 +1,10 @@ +تاسکی نگارش ۱۳٫۰ + +- پشتیبانی از یادداشتهای نمایه (ویژگی ماستودون ۳٫۲٫۰) +- پشتیبانی از اعلامیههای مدیر (ویژگی ماستودون ۳٫۱٫۰) + +- آواتار حساب گزیدهتان در نوار ابزار اصلی نشان داده میشود +- زدن روی نام نمایشی در خط زمانی، نمایهٔ آن کاربر را میگشاید + +- کلّی رفع اشکال و بهبودهای جزیی +- بازگردانیهای بهبودیافته diff --git a/fastlane/metadata/android/fa/changelogs/80.txt b/fastlane/metadata/android/fa/changelogs/80.txt new file mode 100644 index 0000000..f8e0c39 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/80.txt @@ -0,0 +1,7 @@ +تاسکی نگارش ۱۴٫۰ + +- هنگامی که کاربر پیگرفتهای بوق میزند، آگاه شوید - نقشک زنگ روی نمایهاش را بزنید! (ویژگی ماستودون ۳٫۳٫۰) +- ویژگی پیشنویس در تاسکی برای سریعتر، کاربرپسندتر و کممشکلتر بودن، به کلّی باز طرّاحی شده. +- حالت سلامتی جدیدی افزوده شده ه میگذارد ویژگیهای خاصی را در تاسکی محدود کنید. +- تاسکی اکنون میتواند پویانمایی اموجیهای شخصی را نشان دهد. +گزارش تغییر کامل: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/fa/changelogs/82.txt b/fastlane/metadata/android/fa/changelogs/82.txt new file mode 100644 index 0000000..c1ba7e8 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/82.txt @@ -0,0 +1,5 @@ +تاسکی نگارش ۱۵٫۰ + +- درخواستهای پیگیری اکنون همواره در فهرست اصلی نشان داده میشوند. +- گزینشگر زمان برای زمانبندی یک فرسته، اکنون ظاهری هماهنگ با باقی کاره دارد. +گزارش تغییر کامل: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/fa/changelogs/83.txt b/fastlane/metadata/android/fa/changelogs/83.txt new file mode 100644 index 0000000..dd666af --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/83.txt @@ -0,0 +1,3 @@ +تاسکی نگارش ۱۵٫۱ + +این ارائه، فروپاشیای هنگام شرح نوشتن بر تصویرها را تعمیر میکند diff --git a/fastlane/metadata/android/fa/changelogs/87.txt b/fastlane/metadata/android/fa/changelogs/87.txt new file mode 100644 index 0000000..ec05861 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/87.txt @@ -0,0 +1,8 @@ +تاسکی ن۱۶٫۰ + +- منطق بار کردن خط زمانی برای سریعتر بودن، کماشکال بودن و نگهداری سادهتر به طور کامل از نو نوشته شد. +- تاسکی اکنون میتواند اموجیهای پویای شخصی را در قالبAPG و WebP پویا نشان دهد. +- کلّی رفع اشکال +- پشتیبانی اندروید ۱۱ +- ترجمههای جدید: گالیک اسکاتلندی، گالیسیایی، اوکراینی +- ترجمههای بهبودیافته diff --git a/fastlane/metadata/android/fa/changelogs/89.txt b/fastlane/metadata/android/fa/changelogs/89.txt new file mode 100644 index 0000000..971a841 --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/89.txt @@ -0,0 +1,7 @@ +تاسکی ن۱۷٫۰ + +- اکنون هنگام استفاده از چندین حساب «گشودن به عنوان…» در فهرست روی نمایههای حساب نیز موجود است +- ورود اکنون در نمای وبی درون کاره مدیریت میشود +- پشتیبانی از اندروید ۱۲ +- پشتیبانی از میانای جدید پیکربندی نمونهٔ ماستودون +- و بسیاری از بهبودها و رفع مشکلات کوچک diff --git a/fastlane/metadata/android/fa/changelogs/91.txt b/fastlane/metadata/android/fa/changelogs/91.txt new file mode 100644 index 0000000..71c5a0a --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/91.txt @@ -0,0 +1,6 @@ +تاسکی نگارش ۱۸٫۰ + +- پشتیبانی از گونههای آگاهی جدید ماستودون ۳٫۵ +- نشان بات اکنون ظاهر بهتری داشته و با زمینهٔ گزیده تنظیم میشود +- متنها اکنون میتوانند در نمای جزییات فرسته، گزیده شوند +- رفع کلّی مشکل، از جمله مشکلی که جلوی ورود روی اندروید ۶ و پایینتر را میگرفت diff --git a/fastlane/metadata/android/fa/changelogs/94.txt b/fastlane/metadata/android/fa/changelogs/94.txt new file mode 100644 index 0000000..75c4e6e --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/94.txt @@ -0,0 +1,9 @@ +تاسکی ۱۹٫۰ + +- پشتیبانی از Unified Push. برای فعّال کردن این پشتیبانی باید دوباره به حسابهایتان وارد شوید. +- اکنون تعداد پاسخها به فرستهها در خطهای زمانی نشان داده میشود. +- اکنون تصویرها میتوانند هنگام ایجاد فرسته بریده شوند. +- اکنون نمایهها تاریخ ایجادشان را نشان میدهند. +- اکنون عنوان سیاهه هنگام دیدنش در نوار ابزار نمایش داده میشود. +- یک عالمه رفع اشکال +- بهبودهای ترجمه diff --git a/fastlane/metadata/android/fa/changelogs/97.txt b/fastlane/metadata/android/fa/changelogs/97.txt new file mode 100644 index 0000000..2345c1d --- /dev/null +++ b/fastlane/metadata/android/fa/changelogs/97.txt @@ -0,0 +1,9 @@ +تاسکی ۲۰٫۰ + +- نقشککارهٔ جدید به دست https://dzuk.zone +- اکنون میتوانید برچسبها را دنبال کنید. روی برچسبی زده و سپس نقشک داخل نوارابزار را بزنید. +- پشتیبانی از اندروید ۱۳ +- پایینافتادنی جدید در نمای نوشتن برای تنظیم زبان فرسته +- زبانهٔ رسانه در نمایه اکنون به رسانههای حسّاس احترام گذاشته و نرمتر بار میشود. +- اکنون میتوان پیش از فرستادن تصویر، نقطهٔ تمرکز را تنظیم کرد +- گزینهٔ جدید برای نمایش نام کاربری کاملتان در نوارابزار diff --git a/fastlane/metadata/android/fa/full_description.txt b/fastlane/metadata/android/fa/full_description.txt new file mode 100644 index 0000000..5958cb5 --- /dev/null +++ b/fastlane/metadata/android/fa/full_description.txt @@ -0,0 +1,12 @@ +تاسکی کارخواهی سبک برای ماستودون، یک کارساز شبکهٔ اجتماعی نرمافزار آزاد است. + +• طرّاحی متریال +• پیادهسازی بیشتر میاناهای ماستودون +• پشتیبانی از چند حساب +• پشتیبانی از زمینهٔ تاریک و روشن با امکان تغییر خودکار بر اساس زمان روز +• پیشنویسها - ایجاد بوقها و ذخیرهشان برای بعد +• گزینش بین سبکهای مختلف اموجی +• بهینهشده برای همهٔ اندازههای صفحه +• کاملاُ نرمافزار آزاد - بدون وابستگیهای انحصاری مانند خدمات گوگل + +برای دریافت اطّلاعات بیشتر دربارهٔ ماستودون، https://joinmastodon.org را ببینید diff --git a/fastlane/metadata/android/fa/short_description.txt b/fastlane/metadata/android/fa/short_description.txt new file mode 100644 index 0000000..4dcadf7 --- /dev/null +++ b/fastlane/metadata/android/fa/short_description.txt @@ -0,0 +1 @@ +کارخواهی چندحسابه برای شبکهٔ اجتماعی ماستودون diff --git a/fastlane/metadata/android/fa/title.txt b/fastlane/metadata/android/fa/title.txt new file mode 100644 index 0000000..57a4e89 --- /dev/null +++ b/fastlane/metadata/android/fa/title.txt @@ -0,0 +1 @@ +تاسکی diff --git a/fastlane/metadata/android/fi/title.txt b/fastlane/metadata/android/fi/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/fi/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/fr/changelogs/100.txt b/fastlane/metadata/android/fr/changelogs/100.txt new file mode 100644 index 0000000..e22e173 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Prise en charge de l'édition des statuts +- Nouveau réglage pour contrôler l'ordre de lecture +- Vignettes média plus grandes et avec un nouveau macaron indiquant les médias ayant une légende +- Il est à présent possible d'ajouter un compte à une liste depuis son profil +et bien plus diff --git a/fastlane/metadata/android/fr/changelogs/103.txt b/fastlane/metadata/android/fr/changelogs/103.txt new file mode 100644 index 0000000..d2e07c3 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/103.txt @@ -0,0 +1,18 @@ +Tusky 22.0 beta 1 + +Ajouts, dont : + +- Consultation des hashtags tendance +- Modification des légendes et du point focal des images +- Actualisation via menu pour l'accessibilité +- Prise en charge des filtres de Mastodon v4 +- Détail des différences d'un post modifié +- Option pour l'affichage des statistiques des posts dans les fils + +Corrections, dont : + +- Affichage des contrôles lors de la lecture audio +- Correction du calcul de la longueur d'un post +- Publication des légendes d'images + +et bien plus diff --git a/fastlane/metadata/android/fr/changelogs/104.txt b/fastlane/metadata/android/fr/changelogs/104.txt new file mode 100644 index 0000000..9a36c5b --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/104.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 2 + +Corrections, dont : + +- Amélioration de la vitesse de chargement des notifications +- Restauration de l'affichage 0/1/>1 des réponses +- Afficher le nom du titre et non pas ses mots-clés sur les publications filtrées +- Correction du bug où appuyer sur une publication pouvait ouvrir un lien sans rapport +- Affichage du bouton « Ajout » au bon endroit lorsqu'il n'y a aucun filtre +- Correction de divers plantages diff --git a/fastlane/metadata/android/fr/changelogs/58.txt b/fastlane/metadata/android/fr/changelogs/58.txt new file mode 100644 index 0000000..22c4090 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/58.txt @@ -0,0 +1,6 @@ +Tusky v6.0 + +- Les filtres de la timeline ont été déplacés dans les Préférences de compte et se synchronisent avec le serveur +- Vous pouvez personnaliser un onglet avec un hashtag dans la fenêtre principale +- Les listes peuvent être éditées +- Sécurité : suppression du support pour TLS 1.0 et TLS 1.1, et ajout du support pour TLS 1.3 sur Android 6+ diff --git a/fastlane/metadata/android/fr/changelogs/61.txt b/fastlane/metadata/android/fr/changelogs/61.txt new file mode 100644 index 0000000..10fc025 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Supporte l'affichage, le vote et les notifications des sondages +- Nouveaux boutons pour filtrer l’onglet des notifications et effacer toutes les notifications +- « Effacer & réécrire » vos propres pouets +- Nouvel indicateur qui montre si un compte est un robot sur l’image de profil (peut être désactivé dans les préférences) +- Nouvelles traductions : Norvégien Bokmål et Slovène. diff --git a/fastlane/metadata/android/fr/changelogs/67.txt b/fastlane/metadata/android/fr/changelogs/67.txt new file mode 100644 index 0000000..bd4cd18 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Vous pouvez désormais créer des sondages dans Tusky +- Recherche améliorée +- Nouvelle option dans les Préférences du compte pour toujours déplier les avertissements +- Les avatars dans le menu de navigation ont désormais une forme de rectangle arrondi +- Il est désormais possible de signaler des utilisateurs même si ils n'ont jamais posté de statut +- Tusky refuse désormais de se connecter via une connexion non-sécurisée sur Android 6+ +- Plein de petites améliorations et corrections diff --git a/fastlane/metadata/android/fr/changelogs/68.txt b/fastlane/metadata/android/fr/changelogs/68.txt new file mode 100644 index 0000000..f3e1b3f --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Cette mise à jour assure la compatibilité avec Mastodon 3 et améliore les performances et la stabilité. diff --git a/fastlane/metadata/android/fr/changelogs/70.txt b/fastlane/metadata/android/fr/changelogs/70.txt new file mode 100644 index 0000000..5fb322c --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Vous pouvez maintenant marquer les statuts et lister vos signets dans Tusky. +- Vous pouvez maintenant programmer des pouets avec Tusky. Notez que l'heure que vous sélectionnez doit être d'au moins 5 minutes dans le futur. +- Vous pouvez maintenant ajouter des listes à l'écran principal. +- Vous pouvez désormais publier des pièces jointes audio avec Tusky. + +Et beaucoup d'autres petites améliorations et corrections de bugs ! diff --git a/fastlane/metadata/android/fr/changelogs/72.txt b/fastlane/metadata/android/fr/changelogs/72.txt new file mode 100644 index 0000000..73f36ff --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Notifications à propos des nouvelles demandes d’abonnement en cas de compte verrouillé +- Nouvelles fonctionnalités dans l’écran Préférences : + - désactiver le changement d'onglet + - afficher une confirmation avant de partager un pouet + - afficher les aperçus des liens dans les fils +- Possibilité de mettre en sourdine les conversations +- Les résultats des sondages seront désormais calculés en fonction du nombre de sond·é·s +- Résolution de bugs +- Amélioration des traductions diff --git a/fastlane/metadata/android/fr/changelogs/74.txt b/fastlane/metadata/android/fr/changelogs/74.txt new file mode 100644 index 0000000..f61f152 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Amélioration de l'interface principale - vous pouvez maintenant déplacer les onglets vers le bas +- Lorsque vous mettez un utilisateur en sourdine, vous pouvez désormais décider de désactiver ses notifications +- Vous pouvez maintenant suivre autant de hashtags que vous le souhaitez dans un seul onglet hashtag +- La description des médias s'affiche correctement quelque soit sa taille + +Historique complet : https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/fr/changelogs/77.txt b/fastlane/metadata/android/fr/changelogs/77.txt new file mode 100644 index 0000000..1558ac2 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- prise en charge des notes de profil (fonctionnalité de Mastodon 3.2.0) +- le support des annonces de l'administration (fonctionnalité de Mastodon 3.1.0) + +- l'avatar de votre compte sélectionné apparaîtra désormais dans la barre d'outils principale +- en cliquant sur le nom affiché dans une timeline, la page de profil de cet utilisateur s'ouvrira + +- plein corrections de bugs et de petites améliorations +- l'amélioration des traductions diff --git a/fastlane/metadata/android/fr/changelogs/80.txt b/fastlane/metadata/android/fr/changelogs/80.txt new file mode 100644 index 0000000..88f9f05 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Soyez notifié·e des nouveaux message d’un compte suivi — cliquez l’icône de cloche sur son profil ! (fonctionnalité de Mastodon 3.3.0) +- La fonctionnalité de brouillon a été complètement repensée pour être plus rapide, plus conviviale et moins boguée. +- Un nouveau mode bien-être qui vous permet de limiter certaines fonctions de Tusky a été ajouté. +- Tusky peut désormais animer les émojis personnalisés. +Liste complète des changements : https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/fr/changelogs/82.txt b/fastlane/metadata/android/fr/changelogs/82.txt new file mode 100644 index 0000000..3e7971a --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Les demandes de suivi sont désormais toujours montrées dans le menu principal. +- Le sélecteur de date/heure pour planifier un message a désormais un design cohérent avec le reste de l’application +Liste complète des changements : https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/fr/changelogs/83.txt b/fastlane/metadata/android/fr/changelogs/83.txt new file mode 100644 index 0000000..6b8f2b6 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Cette version corrige un plantage lors de l’ajout de description d’image diff --git a/fastlane/metadata/android/fr/changelogs/87.txt b/fastlane/metadata/android/fr/changelogs/87.txt new file mode 100644 index 0000000..b237fcb --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- Le fonctionnement des fils a été complètement revu de manière à être plus rapide, moins dysfonctionnel et plus simple à maintenir, +- Tusky peut désormais lire les émoticônes animées au format APNG et au format Animated WebP, +- Une quantité considérable de bugs a été résolue, +- Le support pour Android 11 a été mis en place, +- De nouvelles traductions sont disponibles : Gaélique écossais, Galicien et Ukrainien, +- Les traductions existantes ont été améliorées. diff --git a/fastlane/metadata/android/fr/changelogs/89.txt b/fastlane/metadata/android/fr/changelogs/89.txt new file mode 100644 index 0000000..9c8ae3b --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- L'option « Ouvrir comme… » disponible quand plusieurs comptes sont connectés est maintenant aussi accessible depuis le menu sur les profils +- L'identification se fait maintenant par une WebView dans l'application +- Android 12 est pris en charge +- La nouvelle API Mastodon de configuration d'instance est prise en charge +- et beaucoup d'autres petites corrections et améliorations diff --git a/fastlane/metadata/android/fr/changelogs/91.txt b/fastlane/metadata/android/fr/changelogs/91.txt new file mode 100644 index 0000000..e385800 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Les nouveaux types de notifications de Mastodon 3.5 sont maintenant supportés +- Le badge robot est maintenant plus joli et s'adapte au thème choisi +- Il est maintenant possible de sélectionner le texte dans l'écran de détails d'un post +- Beaucoup de bogues résolus, dont un qui empêchait de se connecter sous Android 6 ou inférieur diff --git a/fastlane/metadata/android/fr/changelogs/94.txt b/fastlane/metadata/android/fr/changelogs/94.txt new file mode 100644 index 0000000..15cbac9 --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Les notifications via UnifiedPush sont à présent supportées. Pour les activer vous devrez reconnecter vos comptes. +- Le nombre de réponses est maintenant affiché sur chaque post dans les fils. +- Les images peuvent maintenant être rognées lors de l'écriture d'un message. +- Les profils affichent à présent leur date de création. +- Lorsqu'une liste est affichée, son nom apparaît maintenant dans la barre d'outils. +- Beaucoup de bogues résolus. +- Des améliorations sur les traductions. diff --git a/fastlane/metadata/android/fr/changelogs/97.txt b/fastlane/metadata/android/fr/changelogs/97.txt new file mode 100644 index 0000000..07d9b8e --- /dev/null +++ b/fastlane/metadata/android/fr/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- Nouvelle icône d'application par https://dzuk.zone/ +- Vous pouvez maintenant suivre des mot croisillon. Cliquer sur un mot croisillon puis sur l’icône dans la barre d'outil. +- Support de Android 13 +- Nouveau menu déroulant dans la fenêtre de composition permettant de choisir la langue de la publication. +- L'onglet Media dans les profiles respecte maintenant les images sensibles et charge de façon plus fluide. +- Ajout de la possibilité de choisir la partie visible d'une image avant de publier. +- Nouvelle option pour voir votre nom d'utilisateur complet dans la barre d'outils. diff --git a/fastlane/metadata/android/fr/full_description.txt b/fastlane/metadata/android/fr/full_description.txt new file mode 100644 index 0000000..891faf3 --- /dev/null +++ b/fastlane/metadata/android/fr/full_description.txt @@ -0,0 +1,12 @@ +Tusky est un client léger pour Mastodon, un serveur de réseau social libre et ouvert. + +• Design Material +• La plupart de l'API Mastodon est implémentée +• Support du multi-compte +• Thèmes sombre et clair avec possibilité de basculer automatiquement en fonction de l'heure +• Brouillons — rédigez des pouets et enregistrez-les pour plus tard +• Choisissez parmi différents styles d'émojis +• Optimisé pour toutes les tailles d'écrans +• Entièrement ouvert — aucune dépendance non libre comme les services Google + +Pour en apprendre plus à propos de Mastodon, visitez https://joinmastodon.org/ (anglais) diff --git a/fastlane/metadata/android/fr/short_description.txt b/fastlane/metadata/android/fr/short_description.txt new file mode 100644 index 0000000..bbc2f4e --- /dev/null +++ b/fastlane/metadata/android/fr/short_description.txt @@ -0,0 +1 @@ +Un client multi-compte pour le réseau social Mastodon diff --git a/fastlane/metadata/android/fr/title.txt b/fastlane/metadata/android/fr/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/fr/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/ga/short_description.txt b/fastlane/metadata/android/ga/short_description.txt new file mode 100644 index 0000000..041f207 --- /dev/null +++ b/fastlane/metadata/android/ga/short_description.txt @@ -0,0 +1 @@ +Cliaint ilchuntasach don líonra sóisialta Mastodon diff --git a/fastlane/metadata/android/ga/title.txt b/fastlane/metadata/android/ga/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/ga/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/gd/short_description.txt b/fastlane/metadata/android/gd/short_description.txt new file mode 100644 index 0000000..e811ef8 --- /dev/null +++ b/fastlane/metadata/android/gd/short_description.txt @@ -0,0 +1 @@ +Cliant do dh’iomadh cunntas san lìonra sòisealta Mastodon diff --git a/fastlane/metadata/android/gd/title.txt b/fastlane/metadata/android/gd/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/gd/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/gl/changelogs/100.txt b/fastlane/metadata/android/gl/changelogs/100.txt new file mode 100644 index 0000000..4d04458 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Soporte para edición de publicacións +- Novo axuste para controlar a dirección de lectura +- Vistas previas de maior tamaño e indicador de descrición da imaxe +- Agora podes engadir contas ás listas desde o seu perfil +e moito máis diff --git a/fastlane/metadata/android/gl/changelogs/103.txt b/fastlane/metadata/android/gl/changelogs/103.txt new file mode 100644 index 0000000..f43b290 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/103.txt @@ -0,0 +1,18 @@ +Tusky 22.0 beta 1 + +Características incluídas: + +- Ver cancelos en voga +- Edición da descrición das imaxes e punto focal +- Menú "actualizar" para a accesibilidade +- Soporte para filtros de Mastodon v4 +- Mostra as diferencias polo miúdo cando unha publicación se edita +- Opción para mostrar estatísticas da publicación na cronoloxía + +Arranxos: + +- Mostrar controis de reprodución durante reprodución de audio +- Correxir o cálculo da lonxitude da publicación +- Publicar sempre descrición da imaxe + +e moito máis diff --git a/fastlane/metadata/android/gl/changelogs/104.txt b/fastlane/metadata/android/gl/changelogs/104.txt new file mode 100644 index 0000000..569d610 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/104.txt @@ -0,0 +1,11 @@ +Tusky 22.0 beta 2 + +Correccións que inclúen: + +- Velocidade de carga de notificacións mellorada +- Restaurar mostrando 0/1/1+ para as respostas +- Mostra títulos de filtro, non palabras clave de filtro, nas publicacións filtradas +- Corrixiuse un erro no que abrir un estado podía abrir unha ligazón non relacionada +- Mostrar o botón "Engadir" no lugar correcto cando non haxa filtros +- Arranxáronse varios fallos + (trad. automática) diff --git a/fastlane/metadata/android/gl/changelogs/105.txt b/fastlane/metadata/android/gl/changelogs/105.txt new file mode 100644 index 0000000..e7396ad --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/105.txt @@ -0,0 +1,12 @@ +Tusky 22.0 beta 3 + +Correccións que inclúen: + +- Arranxouse o fallo ao ver un fío +- Filtros Mastodon de procesamento de fallos corrixidos +- Pódense facer clic nas ligazóns da bios das notificacións de solicitude de seguimento/seguimento +- Actualizacións de notificacións de Android + - A notificación de Android para unha notificación de Mastodon só debe mostrarse unha vez + - As notificacións de Android agrúpanse por tipo de notificación de Mastodon (seguir, mencionar, aumentar, etc.) + - Eliminouse o potencial de notificacións que faltan +(trad. auto) diff --git a/fastlane/metadata/android/gl/changelogs/106.txt b/fastlane/metadata/android/gl/changelogs/106.txt new file mode 100644 index 0000000..dcd4c90 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/106.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 4 + +Arranxo: + +- das notificacións repetidas se hai varias contas diff --git a/fastlane/metadata/android/gl/changelogs/107.txt b/fastlane/metadata/android/gl/changelogs/107.txt new file mode 100644 index 0000000..d39366a --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/107.txt @@ -0,0 +1,7 @@ +Tusky 22.0 beta 5 + +Correccións: + +- Retrocedeu a biblioteca APNG para corrixir emojis animados rotos +- Garda a copia local do marcador de notificación no caso de que o servidor non admita a API +(trad. automática) diff --git a/fastlane/metadata/android/gl/changelogs/108.txt b/fastlane/metadata/android/gl/changelogs/108.txt new file mode 100644 index 0000000..6ae3be0 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/108.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 6 + +Arranxos: + +- gardar con maior frecuencia a posición de lectura na pestana de Notificacións diff --git a/fastlane/metadata/android/gl/changelogs/109.txt b/fastlane/metadata/android/gl/changelogs/109.txt new file mode 100644 index 0000000..b56a348 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/109.txt @@ -0,0 +1,12 @@ +Tusky 22.0 beta 7 + +Correccións: + + +### Correccións de erros importantes + +- Obtén todas as notificacións pendentes de Mastodon ao crear notificacións de Android +- Facendo clic en "Redactar" desde unha notificación, establecerase a conta incorrecta +- Asegúrate de gardar o "ID de notificación da última lectura" na conta correcta + +(trad. automática) diff --git a/fastlane/metadata/android/gl/changelogs/110.txt b/fastlane/metadata/android/gl/changelogs/110.txt new file mode 100644 index 0000000..1aacdba --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/110.txt @@ -0,0 +1,20 @@ +Tusky 22.0 + +Novidades: + +- Ver cancelos en voga +- Seguir novos cancelos +- Melloras na orde de selección de idiomas +- Mostra a diferenza entre versións dunha publicación +- Soporte para os filtros de Mastodon v4 +- Opción para mostrar na cronoloxía estatítiscas das enquisas +- E máis... + +Arranxos: + +- Lembrar lapela e posición +- Manter as notificacións ata que son lidas +- Disposición correcta de texto RTL e LTR misturados nos perfís +- Cálculo correcto da lonxitude da publicación +- Publicar sempre descrición das imaxes +- E máis... diff --git a/fastlane/metadata/android/gl/changelogs/111.txt b/fastlane/metadata/android/gl/changelogs/111.txt new file mode 100644 index 0000000..f1e102f --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/111.txt @@ -0,0 +1,17 @@ +Tusky 23.0 beta 1 + +Novas características: + +- Nova preferencia para escalar o texto da IU + +Correccións: + +- Garda a información da conta correctamente +- notificacións "pull" en dispositivos con versións de Android <= 11 +- Soluciona un erro de Android onde os campos de texto poden "esquecer" que poden copiar/pegar +- A visualización de "diferencias" no historial de edicións non se estenderá fóra do bordo da pantalla +- Non falla se o teu servidor non ten historial de edición posterior +- Engade un botón "Eliminar" ao editar un filtro +- Mostrar emoji non cadrados correctamente + +(trad. automática) diff --git a/fastlane/metadata/android/gl/changelogs/112.txt b/fastlane/metadata/android/gl/changelogs/112.txt new file mode 100644 index 0000000..c787c01 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/112.txt @@ -0,0 +1,6 @@ +Tusky 23.0 beta 2 + +Arranxos: + +- fallo potencia ao editar os campos do perfil +- menú contextual demasiado grando ao editar a descrición das imaxes diff --git a/fastlane/metadata/android/gl/changelogs/113.txt b/fastlane/metadata/android/gl/changelogs/113.txt new file mode 100644 index 0000000..c69bd48 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/113.txt @@ -0,0 +1,15 @@ +Tusky 23.0 + +Novas características: + +- Novo axuste para o tamaño da letra na interface + +Arranxos: + +- Gardar correctamente a información da conta +- "obter" notificacións en dispositivos con versión Android <=11 +- Problema en Android onde os campos de textos "esquecían" que poden copiar/pegar +- Ver os cambios no historial de edicións e que non se extenda polos bordos da pantalla +- Non falla se o servidor non ten o historial de edicións da publicación +- Mostrar correctamente os emoji que non son cadrados +- Posible fallo ao editar os campos do perfil de usuaria diff --git a/fastlane/metadata/android/gl/changelogs/115.txt b/fastlane/metadata/android/gl/changelogs/115.txt new file mode 100644 index 0000000..c8a1b46 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/115.txt @@ -0,0 +1,10 @@ +Tusky 24.0 + +- As citas e o código agora locen mellor nas publicacións. +- Restablecemos o comportamento antigo na pestana de notificacións. +- As insignias dos roles son visibles nos perfís. +- Melloramos o reprodutor de vídeo. Podes escoller a velocidade de reprodución. +- Nova opción para a aparencia, poder usar o decorado negro seguindo a sistema. +- Nova vista para as publicacións en voga, dispoñible tanto no menú como pestana personal. + +E moitos outros arranxos e melloras! diff --git a/fastlane/metadata/android/gl/changelogs/117.txt b/fastlane/metadata/android/gl/changelogs/117.txt new file mode 100644 index 0000000..e9b9586 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/117.txt @@ -0,0 +1,7 @@ +Tusky 24.1 + +- A pantalla permanece acendida mentras se reproduce un vídeo +- Arranxadas as fugas de memoria. Debería mellorar o rendemento e estabilidade. +- Os emojis cóntanse correctamente como 1 caracter ao escribir unha mensaxe. +- Arranxo do fallo nalgúns dispositivos ao seleccionar texto. +- As iconas nos texto de axuda en cronoloxías baleiras xa estarán aliñados correctamente. diff --git a/fastlane/metadata/android/gl/changelogs/119.txt b/fastlane/metadata/android/gl/changelogs/119.txt new file mode 100644 index 0000000..e3f5e4f --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/119.txt @@ -0,0 +1,8 @@ +Tusky 25 + +- soporte para a API de tradución de Mastodon +- mostra o idioma da publicación +- mellora nas transicións entre pantallas +- os axustes dos filtros agora están nas preferencias da conta +- as estatísticas da publicación están nunha posición estable +- moitos cambios internos para mellorar a estabilidade e rendemento diff --git a/fastlane/metadata/android/gl/changelogs/58.txt b/fastlane/metadata/android/gl/changelogs/58.txt new file mode 100644 index 0000000..e52735a --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/58.txt @@ -0,0 +1,9 @@ +Tusky v6.0 + +- Timeline filters have moved to Account Preferences and will sync with the server +- You can now have a custom hashtag as tab in the main interface +- Lists can now be edited +- Security: removed support for TLS 1.0 and TLS 1.1, and added support for TLS 1.3 on Android 6+ +- The compose view will now suggest custom emojis when starting to type +- New theme setting "follow system theme +https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/gl/changelogs/61.txt b/fastlane/metadata/android/gl/changelogs/61.txt new file mode 100644 index 0000000..ea49846 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Support for displaying polls, voting and poll notifications +- New buttons to filter the notification tab and to delete all notifications +- delete & redraft your own toots +- new indicator that shows if an account is a bot on the profile image (can be turned off in the preferences) +- New translations: Norwegian Bokmål and Slovenian. diff --git a/fastlane/metadata/android/gl/changelogs/67.txt b/fastlane/metadata/android/gl/changelogs/67.txt new file mode 100644 index 0000000..dfeb195 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Podes crear Enquisas desde Tusky +- Busca mellorada +- Nova opción nas Preferencias da Conta para despregar avisos de contido +- Os avatares na caixa de navegación agora son cadrados con bordo redondeado +- Xa podes denunciar usuarias incluso se nunca publicaron +- Tusky rexeitará conectar sobre conexións sen cifrar en Android 6+ +- Múltiples melloras e corrección de fallos diff --git a/fastlane/metadata/android/gl/changelogs/68.txt b/fastlane/metadata/android/gl/changelogs/68.txt new file mode 100644 index 0000000..6453e93 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Esta versión asegura a compatibilidade con Mastodon 3 e mellora o rendemento e estabilidade. diff --git a/fastlane/metadata/android/gl/changelogs/70.txt b/fastlane/metadata/android/gl/changelogs/70.txt new file mode 100644 index 0000000..eb24287 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- You can now bookmark statuses & list your bookmarks in Tusky. +- You can now schedule toots with Tusky. Note that the time you select has to be at least 5 minutes in the future. +- You can now add lists to the main screen. +- You can now post audio attachments with Tusky. + +And a lot of other small improvements and bug fixes! diff --git a/fastlane/metadata/android/gl/changelogs/72.txt b/fastlane/metadata/android/gl/changelogs/72.txt new file mode 100644 index 0000000..0ac5bb3 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/72.txt @@ -0,0 +1,9 @@ +Tusky v11. + +- Notifications about new follow requests when your account is locked +- New features that can be toggled on the Preferences screen: + - disable swiping between tabs + - show a confirmation dialog before boosting a toot + - show link previews in timelines +- Conversations can now be muted +- https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/gl/changelogs/74.txt b/fastlane/metadata/android/gl/changelogs/74.txt new file mode 100644 index 0000000..1b44b6d --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Melloras na interface - podes mover lapela á zona inferior +- Ao acalar unha usuaria, agora tamén podes decidir se acalas as súas notificacións +- Podes seguir cantos cancelos queiras nunha única lapela de cancelos +- Melloras no xeito en que se mostran as descricións do multimedia, útil para descricións moi longas +... +Rexistro completo dos cambios en https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/gl/changelogs/77.txt b/fastlane/metadata/android/gl/changelogs/77.txt new file mode 100644 index 0000000..05db67c --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- soporte para notas do perfil (Mastodon >3.2.0) +- soporte para anuncios da admin (Mastodon >3.1.0) + +- móstrase o avatar da túa conta seleccionada na barra de ferramentas principal +- premendo no nome público nunha cronoloxía abre a páxina de perfil desa usuaria + +- corrección de múltiples erros e pequenas melloras +- melloras nas traducións diff --git a/fastlane/metadata/android/gl/changelogs/80.txt b/fastlane/metadata/android/gl/changelogs/80.txt new file mode 100644 index 0000000..cbef321 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Recibe notificacións cando alguén que segues publica - preme na icona da campá no seu perfil! (Mastodon >3.3.0) +- Os borradores en Tusky foron redeseñados para ser máis rápidos, amigables e con menos fallos. +- Novo modo benestar que che permite limitar certas características engadidas a Tusky. +- Tusky xa pode animar os emojis personalizados. +Rexistro completo dos cambios: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/gl/changelogs/82.txt b/fastlane/metadata/android/gl/changelogs/82.txt new file mode 100644 index 0000000..99482d8 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- As solicitudes de seguimento móstranse sempre no menú principal. +- O selector para programar unha publicación ten un deseño consistente co resto da aplicación +Rexistro completo dos cambios: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/gl/changelogs/83.txt b/fastlane/metadata/android/gl/changelogs/83.txt new file mode 100644 index 0000000..60ed158 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Esta versión arranxa o problema coa descrición de imaxes diff --git a/fastlane/metadata/android/gl/changelogs/87.txt b/fastlane/metadata/android/gl/changelogs/87.txt new file mode 100644 index 0000000..1a7e99d --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- Cambiamos completamente a lóxica de carga da cronoloxía para facela máis rápida, con menos fallos e doada de manter. +- Tusky pode animar os emojis personalizados en APNG & formato WebP Animado. +- Arranxamos moitos problemas. +- Soporte para Android 11 +- Novas traducións: Gaélico de Escocia, Galego, Ucraíno +- Mellora nas traducións diff --git a/fastlane/metadata/android/gl/changelogs/89.txt b/fastlane/metadata/android/gl/changelogs/89.txt new file mode 100644 index 0000000..238ea8c --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Abrir como..." agora está dispoñible no menú dos perfís da conta ao usar varias contas +- O Acceso agora xestionase mediante WebView dentro da app +- Soporte para Android 12 +- Soporte para a nova cofiguración da API das instancias de Mastodon +- e un feixe de pequenas melloras e arranxos diff --git a/fastlane/metadata/android/gl/changelogs/91.txt b/fastlane/metadata/android/gl/changelogs/91.txt new file mode 100644 index 0000000..0001769 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Soporte para o novos tipos de notificación de Mastodon 3.5 +- A insignia de bot foi redeseñada e combina mellor co decorado seleccionado +- Podes seleccionar texto na vista de detalles da publicación +- Moitos arranxos adicionais, incluíndo o que non permitía acceder en Android <6 diff --git a/fastlane/metadata/android/gl/changelogs/94.txt b/fastlane/metadata/android/gl/changelogs/94.txt new file mode 100644 index 0000000..0e4befb --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Soporte para Unified Push. Para activar a función tes que volver a acceder ás túas contas. +- Agora aparece nas cronoloxías o número de respostas a unha publicación. +- Podes recortar as imaxes cando escribes unha publicación. +- Os perfís mostran a data na que foron creados. +- Móstrase o título da lista na barra de ferramentas ao visualizala. +- Arranxamos moitos fallos. +- Melloras nas traducións. diff --git a/fastlane/metadata/android/gl/changelogs/97.txt b/fastlane/metadata/android/gl/changelogs/97.txt new file mode 100644 index 0000000..5c707f7 --- /dev/null +++ b/fastlane/metadata/android/gl/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- Nova icona da app por Dzuk https://dzuk.zone/ +- Podes seguir cancelos. Preme no cancelo e depois na icona da barra de ferramentas. +- Soporte para Android 13 +- Cando escribes unha mensaxe podes seleccionar o idioma da publicación +- Nos perfís, a lapela multimedia agora respecta o marcado como sensible e carga máis suavemente. +- É posible establecer o foco nunha zona da imaxe antes de publicala +- Nova opcións para mostrar o identificador de usuaria completo na barra de ferramentas diff --git a/fastlane/metadata/android/gl/full_description.txt b/fastlane/metadata/android/gl/full_description.txt new file mode 100644 index 0000000..0bd1138 --- /dev/null +++ b/fastlane/metadata/android/gl/full_description.txt @@ -0,0 +1,12 @@ +Tusky é un cliente lixeiro para Mastodon, un servidor libre e de código aberto para a web social. + +• Material Design +• Maioría das APIs de Mastodon implementadas +• Soporte multi-conta +• Decorado escuro e claro coa posibilidade de cambio automático segundo a hora do día +• Borradores - compoñer toots e gardalos para máis tarde +• Escoller entre varios estilos de emoji +• Optimizado para todos os tamaños de pantalla +• Completamente de código aberto - sen dependencias non-libres como os servizos de Google + +Coñece máis acerca de Mastodon, visita https://joinmastodon.org/ diff --git a/fastlane/metadata/android/gl/short_description.txt b/fastlane/metadata/android/gl/short_description.txt new file mode 100644 index 0000000..fb536b2 --- /dev/null +++ b/fastlane/metadata/android/gl/short_description.txt @@ -0,0 +1 @@ +Cliente multi conta para a rede social Mastodon diff --git a/fastlane/metadata/android/gl/title.txt b/fastlane/metadata/android/gl/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/gl/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/hi/changelogs/68.txt b/fastlane/metadata/android/hi/changelogs/68.txt new file mode 100644 index 0000000..4fd9e54 --- /dev/null +++ b/fastlane/metadata/android/hi/changelogs/68.txt @@ -0,0 +1,3 @@ +टस्की v9.1 + +यह रिलीज मस्तोडोन 3 के साथ संगतता सुनिश्चित करती है और प्रदर्शन तथा स्थिरता में सुधार करती है। diff --git a/fastlane/metadata/android/hi/short_description.txt b/fastlane/metadata/android/hi/short_description.txt new file mode 100644 index 0000000..73a5731 --- /dev/null +++ b/fastlane/metadata/android/hi/short_description.txt @@ -0,0 +1 @@ +सोशल नेटवर्क मास्टोडन के लिए एक मल्टी अकाउंट क्लाइंट diff --git a/fastlane/metadata/android/hi/title.txt b/fastlane/metadata/android/hi/title.txt new file mode 100644 index 0000000..a1a2e8d --- /dev/null +++ b/fastlane/metadata/android/hi/title.txt @@ -0,0 +1 @@ +टस्की diff --git a/fastlane/metadata/android/hu/changelogs/100.txt b/fastlane/metadata/android/hu/changelogs/100.txt new file mode 100644 index 0000000..68ae394 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Támogatás bejegyzések szerkesztéséhez +- Új beállítás az előnyben részesített olvasási irány állításához +- Nagyobb médiaelőnézetek és új áfedés a leírásokkal rendelkező média jelzéséhez +- Már lehetséges fiókokat hozzáadni listákhoz a profilnézetből is +és sok más diff --git a/fastlane/metadata/android/hu/changelogs/103.txt b/fastlane/metadata/android/hu/changelogs/103.txt new file mode 100644 index 0000000..8496a05 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/103.txt @@ -0,0 +1,18 @@ +Tusky 22.0 beta 1 + +Új funkciók: + +- Trendi hashtag-ek megtekintése +- Képleírás és fókuszpont szerkesztése +- "Frissítés" menü az elérhetőségért +- Mastodon v4 szűrők támogatása +- Részletes különbségek mutatása, amikor egy bejegyzést szerkesztettek +- Opció bejegyzés-statisztikák idővonalon való megjelenítésére + +Javítások: + +- Lejátszó vezérlőinek mutatása hangvisszajátszás közben +- Bejegyzés hosszkalkuláció javítása +- Mindig közzétesszük a képfeliratokat + +és még sok más diff --git a/fastlane/metadata/android/hu/changelogs/104.txt b/fastlane/metadata/android/hu/changelogs/104.txt new file mode 100644 index 0000000..fe98679 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/104.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 2 + +Javítások: + +- Javított értesítés-betöltési sebesség +- Újra mutatjuk a 0/1/1+ a válaszoknál +- Szűrőcímek mutatása, nem a kulcsszavaké a szűrt bejegyzéseken +- Hibajavítás: egy bejegyzés megnyitása megnyithatott egy nem kapcsolódó hivatkozást +- Mutassuk a "Hozzáadás" a megfelelő helyen, ha nincsenek szűrők +- Különböző programleállások javítása diff --git a/fastlane/metadata/android/hu/changelogs/105.txt b/fastlane/metadata/android/hu/changelogs/105.txt new file mode 100644 index 0000000..4457628 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/105.txt @@ -0,0 +1,11 @@ +Tusky 22.0 beta 3 + +Javítások: + +- Szálak nézegetése közbeni programleállás javítása +- Mastodon szűrők feldolgozása közbeni programleállás javítása +- Profilokon a követési hivatkozások kattinthatóak +- Androidos értesítés-kezelési frissítések + - Android értesítés a Mastodonról csak egyszer mutatandó + - Android értesítések Mastodon értesítési típus (követés, említés, megtolás, stb) szerint vannak csoportosítva + - Lehetséges értesítés-elhagyás megoldva diff --git a/fastlane/metadata/android/hu/changelogs/106.txt b/fastlane/metadata/android/hu/changelogs/106.txt new file mode 100644 index 0000000..25cf2ab --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/106.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 4 + +Javítások: + +- Több fiók használata esetén az értesítések újra történő lekérésének megoldása diff --git a/fastlane/metadata/android/hu/changelogs/107.txt b/fastlane/metadata/android/hu/changelogs/107.txt new file mode 100644 index 0000000..fd151c7 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/107.txt @@ -0,0 +1,6 @@ +Tusky 22.0 beta 5 + +Javítások: + +- APNG könytár vissza a régire a törött animált emojik javításáért +- Értesítésjelző helyi mentése arra az esetre, ha a kiszolgáló nem támogatja az API-t diff --git a/fastlane/metadata/android/hu/changelogs/108.txt b/fastlane/metadata/android/hu/changelogs/108.txt new file mode 100644 index 0000000..c88d7f6 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/108.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 6 + +Javítások: + +- Értesítési fülön az olvasási pozíció gyakoribb mentése diff --git a/fastlane/metadata/android/hu/changelogs/109.txt b/fastlane/metadata/android/hu/changelogs/109.txt new file mode 100644 index 0000000..277fbd6 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/109.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 7 + +Javítások: + + +### Jelentős hibák + +- Minden fontos Mastodon értesítés lekérése, amikor Android értesítéseket hozunk létre +- A "Üzenetírás" kattintása rossz fiókra vitt, megoldva +- Annak biztosítása, hogy az "utolsó olvasott értesítés azonosítója" a helyes fiókba mentődjön diff --git a/fastlane/metadata/android/hu/changelogs/110.txt b/fastlane/metadata/android/hu/changelogs/110.txt new file mode 100644 index 0000000..60db53a --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/110.txt @@ -0,0 +1,20 @@ +Tusky 22.0 + +Új funkciók: + +- Felkapott hashtag-ek mutatása +- Új hashtag-ek bekövetése +- Jobb sorrendezés nyelvek választásakor +- Bejegyzésverziók közötti különbségek mutatása +- Mastodon v4 szűrők támogatása +- Lehetőség bejegyzés-statisztikák mutatására az idővonalon +- És sok más... + +Javítások: + +- Kiválasztott fül és pozíció megjegyzése +- Értesítések megtartása az elolvasásig +- Kevert balról jobbra és jobbról balra szöveg helyes mutatása a profilon +- Bejegyzéshossz kiszámításának javítása +- Képfeliratok publikálása minden esetben +- És sok más.. diff --git a/fastlane/metadata/android/hu/changelogs/111.txt b/fastlane/metadata/android/hu/changelogs/111.txt new file mode 100644 index 0000000..47aa24d --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/111.txt @@ -0,0 +1,15 @@ +Tusky 23.0 beta 1 + +Új funkciók: + +- Új beállítás UI-szöveg nagyítására + +Javítások: + +- Fiókinfók helyes mentése +- "leküldési" értesítések Android 11-nél régebbi eszközön +- Androidos hibajavítás, ahol a szövegmezők "elfelejtik", hogy tudnak szöveget másolni +- A "különbségek" mutatása a szerkesztési történetben nem túl nagy +- Összeomlás elkerülése, ha a kiszolgáló nem ismeri a bejegyzés történetét +- "Törlés" gomb hozzáadása, amikor szűrőt szerkesztünk +- Nem kocka emojik helyes megjelenítése diff --git a/fastlane/metadata/android/hu/changelogs/58.txt b/fastlane/metadata/android/hu/changelogs/58.txt new file mode 100644 index 0000000..6d85209 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/58.txt @@ -0,0 +1,12 @@ +Tusky v6.0 + +- Az idővonalak szűrői szinkronizálódnak a szerverrel és bekerültek a fiók beállításaiba +- A főképernyőn saját hashtaged lehet tabként +- A listák szerkeszthetőek +- Biztonság: TLS 1.0 és 1.1 eltávolítva, TLS 1.3 támogatása Android 6+-on +- A szerkesztőnézet egyedi emojikat javasol a gépelés megkezdésekor +- Új témabeállítás "rendszertéma követése" +- Javított idővonalak +- Új beállítások: Felülbírálható a rendszer nyelvbeállítása +- Új fordítások: Cseh és Eszperantó +- Rengeteg javítás diff --git a/fastlane/metadata/android/hu/changelogs/61.txt b/fastlane/metadata/android/hu/changelogs/61.txt new file mode 100644 index 0000000..7778048 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Szavazás támogatása +- Új gombok értesítések szűrésére és az összes törlésére +- Tülkök törlése és újrafogalmazása +- Új indikátor, mely mutatja a bot fiókokat (beállításokban kapcsolható) +- Új fordítások: Norwegian, Bokmål és Slovenian. diff --git a/fastlane/metadata/android/hu/changelogs/67.txt b/fastlane/metadata/android/hu/changelogs/67.txt new file mode 100644 index 0000000..e2e0aaf --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Indíthatsz szavazást Tusky-ból +- Fejlettebb keresés +- Új opció a fiók beállításainál a tartalom-figyelmeztetések alapértelmezett mutatására +- A navigációs fióknál az avataroknak lekerekített négyzet alakja van +- Be lehet jelenteni olyan fiókokat is, akik még sosem posztoltak +- A Tusky nem fog csatlakozni titkosítatlan kapcsolaton Android 6+-on +- Egy rakás kisebb fejlesztés és bugfix diff --git a/fastlane/metadata/android/hu/changelogs/68.txt b/fastlane/metadata/android/hu/changelogs/68.txt new file mode 100644 index 0000000..b481182 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Ez a kiadás biztosítja a kompatibilitást a Mastodon 3-mal, valamint javítja a teljesítményt és a stabilitást. diff --git a/fastlane/metadata/android/hu/changelogs/70.txt b/fastlane/metadata/android/hu/changelogs/70.txt new file mode 100644 index 0000000..8894283 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Tülköket megjelölhetsz könyvjelzővel és ezeket listázhatod is. +- Ütemezhetsz tülköket a Tuskyban. Az ütemezés legalább 5 perccel a jövőbe kell szóljon. +- A fő képernyőhöz már listákat is hozzáadhatsz. +- Posztolhatsz hangos csatolmányokat is. + +És sok egyéb apró fejlesztés és hibajavítás! diff --git a/fastlane/metadata/android/hu/changelogs/72.txt b/fastlane/metadata/android/hu/changelogs/72.txt new file mode 100644 index 0000000..7b89bc4 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Értesítés új követési kérelemről akkor is, amikor a fiókod zárolva van +- Új funkciók, melyek a beállításokban kapcsolhatóak: + - tabok közötti váltás csúsztatással + - megerősítés mutatása megtolás előtt + - link előnézet megjelenítése idővonalakon +- Beszélgetések elnémíthatóak +- A szavazások eredményét a szavazók száma alapján számoljuk a szavazatok száma helyett, így a többválaszos szavazások értelmezése könnyebb +- Rengeteg hibajavítás (pl. tülkök írása) +- Javított fordítás diff --git a/fastlane/metadata/android/hu/changelogs/74.txt b/fastlane/metadata/android/hu/changelogs/74.txt new file mode 100644 index 0000000..58df815 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Továbbfejlesztett kezelői felület - a füleket alulra is rakhatod +- Ha egy felhasználót némítasz, külön dönthetsz arról, hogy a tőle érkező értesítéseket némítsd-e +- Az új hashtag fülön annyi hashtaget követhetsz, amennyit csak akarsz +- Továbbfejlesztettük a média leírások megjelenítését, így ez már nagyon hosszú leírásokra is jól működik + +Összes változás: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/hu/changelogs/77.txt b/fastlane/metadata/android/hu/changelogs/77.txt new file mode 100644 index 0000000..43b92b6 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- privát profilmegjegyzések támogatása (Mastodon 3.2.0 funkció) +- adminisztrátori közlemények támogatása (Mastodon 3.1.0 funkció) + +- az éppen használt fiókod avatarja mostantól látszik az eszköztáron +- az idővonalon egy profilra kattintva előjön a felhasználó profiloldala + +- rengeteg hibajavítás és apró fejlesztés +- javított fordítások diff --git a/fastlane/metadata/android/hu/changelogs/80.txt b/fastlane/metadata/android/hu/changelogs/80.txt new file mode 100644 index 0000000..dd5d580 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Értesítést kaphatsz, amikor egy követett felhasználó tülköl - csak kattints a csengő ikonra a profilján! (Mastodon 3.3.0 funkció) +- A Tusky piszkozat funkcióját teljesen újraterveztük, hogy gyorsabb, felhasználóbarátabb, hibamentesebb legyen. +- Az új jóllét üzemmód lehetővé teszi, hogy bizonyos Tusky funkciókat korlátozz. +- A Tusky mostantól képes animálni az egyedi emojikat is. +Összes változás: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/hu/changelogs/82.txt b/fastlane/metadata/android/hu/changelogs/82.txt new file mode 100644 index 0000000..1a5aeb8 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- A követési kérelmeket mindig mutatjuk a főmenüben. +- A posztok időzítésére használt időválasztó kinézete illeszkedik az app többi részéhez. +Összes változtatás: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/hu/changelogs/83.txt b/fastlane/metadata/android/hu/changelogs/83.txt new file mode 100644 index 0000000..a2b9d6c --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Ez a kiadás javít egy képek feliratozása közben jelentkező összeomlást diff --git a/fastlane/metadata/android/hu/changelogs/87.txt b/fastlane/metadata/android/hu/changelogs/87.txt new file mode 100644 index 0000000..81078c7 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- Az idővonal betöltési logikáját teljesen újraírtuk, hogy gyorsabb, hibamentesebb és karbantarthatóbb legyen. +- A Tusky már animálja az APNG és Animated WebP formátumú emodzsikat. +- Sok hibajavítás +- Android 11 támogatás +- Új fordítások: skót, galíciai, ukrán +- Javított fordítások diff --git a/fastlane/metadata/android/hu/changelogs/89.txt b/fastlane/metadata/android/hu/changelogs/89.txt new file mode 100644 index 0000000..e780376 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Megnyit, mint..." már a fiókok profiljainak menüjében is elérhető, amikor több fiókot használsz +- A bejelentkezés az appon belül már WebView-ban működik +- Android 12 támogatása +- új Mastodon szerverkonfigurációs API támogatása +- sok más kisebb javítás és fejlesztés diff --git a/fastlane/metadata/android/hu/changelogs/91.txt b/fastlane/metadata/android/hu/changelogs/91.txt new file mode 100644 index 0000000..c9ad649 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Támogatás az új Mastodon 3.5 értesítési típusokhoz +- A bot jelvény jobban néz ki és alkalmazkodik a választott témához +- A szöveget már kiválaszthatod a bejegyzési részletek megtekintésénél is +- Sok hibajavítás, beleértve egy olyat, mely megakadályozta a bejelentkezést Android 6-on vagy alatta diff --git a/fastlane/metadata/android/hu/changelogs/94.txt b/fastlane/metadata/android/hu/changelogs/94.txt new file mode 100644 index 0000000..fe40fdf --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Egységes leküldés (Unified Push) támogatása. A támogatás aktiválásához újra jelentkezz be a fiókjaidba. +- A bejegyzésekre érkezett válaszok száma már látható az idővonalon. +- Bejegyzés szerkesztése közben meg lehet vágni a képeket. +- A profilokon látható ezek létrehozásának időpontja. +- Lista megtekintésekor ennek címe látható az eszköztáron. +- Rengeteg hibajavítás +- Fordítási javítások diff --git a/fastlane/metadata/android/hu/changelogs/97.txt b/fastlane/metadata/android/hu/changelogs/97.txt new file mode 100644 index 0000000..802ace6 --- /dev/null +++ b/fastlane/metadata/android/hu/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- Új App ikon, készítő Dzuk https://dzuk.zone/ +- A hashtagek már követhetőek. Kattints egy hashtagre aztán az ikonra az eszköztáron. +- Android 13 támogatás +- Új lenyíló a szerkesztőnézetben a bejegyzés nyelvének beállításához +- A profil médiás füle már figyelembeveszi az érzékeny médiát is és finomabban töltődik be. +- Már lehetséges egy kép fókuszpontját elküldés előtt beállítani +- Új lehetőség a teljes felhasználói neved mutatására az eszköztáron diff --git a/fastlane/metadata/android/hu/full_description.txt b/fastlane/metadata/android/hu/full_description.txt new file mode 100644 index 0000000..dead178 --- /dev/null +++ b/fastlane/metadata/android/hu/full_description.txt @@ -0,0 +1,12 @@ +A Tusky egy könnyűsúlyú kliens a Mastodonhoz, mely egy nyílt forráskódú közösségi hálózati kiszolgáló. + +• Material design +• A legtöbb Mastodon API-t megvalósításra került +• Többfiókos támogatás +• Sötét és világos téma, valamint automatikus váltási lehetőség napszak szerint +• Piszkozatok – tülkök készítése, és mentés későbbre +• Emodzsi stílusok közötti választás +• Minden képernyőméretre optimalizálva +• Teljesen nyílt forráskód – nincsenek nem szabad függőségek, mint például a Google szolgáltatások + +Több információ a Mastodonról: https://joinmastodon.org/ diff --git a/fastlane/metadata/android/hu/short_description.txt b/fastlane/metadata/android/hu/short_description.txt new file mode 100644 index 0000000..130eac3 --- /dev/null +++ b/fastlane/metadata/android/hu/short_description.txt @@ -0,0 +1 @@ +Többfiókos kliens a Mastodon közösségi hálóhoz diff --git a/fastlane/metadata/android/hu/title.txt b/fastlane/metadata/android/hu/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/hu/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/id/changelogs/100.txt b/fastlane/metadata/android/id/changelogs/100.txt new file mode 100644 index 0000000..a16f36a --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Dukungan untuk mengedit postingan +- Pengaturan baru untuk mengontrol arah bacaan yang diinginkan +- Pratinjau media yang lebih besar dan tampilan overlay baru untuk menunjukkan media dengan deskripsi +- Kini memungkinkan untuk menambahkan akun ke daftar dari profil mereka +dan banyak lagi diff --git a/fastlane/metadata/android/id/changelogs/58.txt b/fastlane/metadata/android/id/changelogs/58.txt new file mode 100644 index 0000000..ba93a98 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/58.txt @@ -0,0 +1,13 @@ +Tusky v6.0 + +- Filter timeline telah dipindahkan ke Preferensi Akun dan akan disinkronkan dengan server +- Sekarang Anda dapat memiliki hashtag kustom sebagai tab di antarmuka utama +- Daftar sekarang dapat diedit +- Keamanan: dukungan untuk TLS 1.0 dan TLS 1.1 telah dihapus, dan dukungan untuk TLS 1.3 telah ditambahkan pada Android 6+ +- Tampilan tulisan sekarang akan menyarankan emoji kustom saat memulai mengetik +- Pengaturan tema baru "ikuti tema sistem" +- Aksesibilitas timeline yang ditingkatkan +- Tusky sekarang akan mengabaikan notifikasi yang tidak diketahui dan tidak lagi mengalami crash +- Pengaturan baru: Sekarang Anda dapat mengubah bahasa sistem dan mengatur bahasa yang berbeda di Tusky +- Terjemahan baru: Ceko dan Esperanto +- Banyak perbaikan dan peningkatan lainnya diff --git a/fastlane/metadata/android/id/changelogs/61.txt b/fastlane/metadata/android/id/changelogs/61.txt new file mode 100644 index 0000000..2c3a120 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Dukungan untuk menampilkan jajak pendapat, voting, dan notifikasi jajak pendapat +- Tombol baru untuk menyaring tab notifikasi dan menghapus semua notifikasi +- Menghapus dan menyunting ulang toot Anda sendiri +- Indikator baru yang menunjukkan jika akun adalah bot pada gambar profil (dapat dimatikan dalam preferensi) +- Terjemahan baru: Norwegia Bokmål dan Slovenian. diff --git a/fastlane/metadata/android/id/changelogs/67.txt b/fastlane/metadata/android/id/changelogs/67.txt new file mode 100644 index 0000000..62c32f8 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Kini Anda dapat membuat Polling dari Tusky +- Pencarian yang ditingkatkan +- Opsi baru dalam Preferensi Akun untuk selalu memperluas peringatan konten +- Avatar di drawer navigasi sekarang memiliki bentuk kotak yang dibulatkan +- Kini memungkinkan untuk melaporkan pengguna bahkan ketika mereka tidak pernah memposting status +- Tusky sekarang akan menolak untuk terhubung melalui koneksi cleartext pada Android 6+ +- Banyak perbaikan kecil dan perbaikan bug lainnya diff --git a/fastlane/metadata/android/id/changelogs/68.txt b/fastlane/metadata/android/id/changelogs/68.txt new file mode 100644 index 0000000..96267e2 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Rilis ini memastikan kompatibilitas dengan Mastodon 3 dan meningkatkan performa dan stabilitas. diff --git a/fastlane/metadata/android/id/changelogs/70.txt b/fastlane/metadata/android/id/changelogs/70.txt new file mode 100644 index 0000000..f014a49 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Anda sekarang dapat bookmark status & daftar bookmark Anda di Tusky. +- Anda sekarang dapat menjadwalkan toot dengan Tusky. Perhatikan bahwa waktu yang Anda pilih harus setidaknya 5 menit di masa mendatang. +- Anda sekarang dapat menambahkan daftar ke layar utama. +- Anda sekarang dapat memposting lampiran audio dengan Tusky. + +Dan banyak perbaikan kecil dan perbaikan bug lainnya! diff --git a/fastlane/metadata/android/id/changelogs/72.txt b/fastlane/metadata/android/id/changelogs/72.txt new file mode 100644 index 0000000..6f46073 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Notifikasi tentang permintaan mengikuti baru ketika akun Anda terkunci +- Fitur baru yang dapat dipilih di layar Preferensi: + - Nonaktifkan swipe antar tab + - Menampilkan dialog konfirmasi sebelum boosting sebuah toot + - Menampilkan pratinjau tautan dalam timeline +- Percakapan sekarang dapat dimute +- Hasil jajak pendapat sekarang akan dihitung berdasarkan jumlah pemilih dan bukan jumlah suara total yang membuat jajak pendapat multichoice lebih mudah dipahami +- Banyak perbaikan bug, sebagian besar terkait dengan menyusun toot +- Terjemahan yg diperbarui diff --git a/fastlane/metadata/android/id/changelogs/74.txt b/fastlane/metadata/android/id/changelogs/74.txt new file mode 100644 index 0000000..302566d --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Antarmuka utama yang ditingkatkan - sekarang Anda dapat memindahkan tab ke bagian bawah +- Saat mem-mute pengguna, Anda sekarang dapat memutuskan apakah akan mem-mute notifikasi mereka +- Anda sekarang dapat mengikuti sebanyak mungkin hashtag yang Anda inginkan dalam satu tab hashtag tunggal +- Meningkatkan cara deskripsi media ditampilkan sehingga dapat bekerja bahkan untuk deskripsi yang sangat panjang + +Changelog lengkap: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/id/changelogs/77.txt b/fastlane/metadata/android/id/changelogs/77.txt new file mode 100644 index 0000000..8262b55 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- Dukungan untuk catatan profil (fitur Mastodon 3.2.0) +- Dukungan untuk pengumuman admin (fitur Mastodon 3.1.0) + +- Avatar akun yang dipilih sekarang akan ditampilkan di toolbar utama +- Mengklik nama tampilan dalam timeline sekarang akan membuka halaman profil pengguna tersebut + +- Banyak perbaikan bug dan perbaikan kecil +- Terjemahan yang ditingkatkan diff --git a/fastlane/metadata/android/id/changelogs/80.txt b/fastlane/metadata/android/id/changelogs/80.txt new file mode 100644 index 0000000..c71341f --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Dapatkan pemberitahuan saat pengguna yang diikuti memposting - klik ikon bel di profil mereka! (fitur Mastodon 3.3.0) +- Fitur draf di Tusky telah sepenuhnya didesain ulang menjadi lebih cepat, lebih ramah pengguna, dan lebih sedikit buggy. +- Mode kesejahteraan baru yang memungkinkan Anda membatasi fitur Tusky tertentu telah ditambahkan. +- Tusky sekarang dapat menganimasikan emoji kustom. +Changelog lengkap: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/id/changelogs/82.txt b/fastlane/metadata/android/id/changelogs/82.txt new file mode 100644 index 0000000..4e49abf --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Permintaan ikuti sekarang selalu ditampilkan di menu utama. +- Pemilih waktu untuk menjadwalkan posting memiliki desain yang konsisten dengan aplikasi lainnya sekarang +Changelog lengkap: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/id/changelogs/83.txt b/fastlane/metadata/android/id/changelogs/83.txt new file mode 100644 index 0000000..d3e2910 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Rilis ini memperbaiki crash saat memberi keterangan pada gambar diff --git a/fastlane/metadata/android/id/changelogs/87.txt b/fastlane/metadata/android/id/changelogs/87.txt new file mode 100644 index 0000000..0d8ede9 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- Logika pemuatan timeline telah sepenuhnya ditulis ulang agar lebih cepat, lebih sedikit buggy dan lebih mudah dipelihara. +- Tusky sekarang dapat menganimasikan emoji khusus dalam format APNG & Animated WebP. +- Banyak perbaikan bug +- Dukungan untuk Android 11 +- Terjemahan baru: Gaelik Skotlandia, Galicia, Ukraina +- Terjemahan yang ditingkatkan diff --git a/fastlane/metadata/android/id/changelogs/89.txt b/fastlane/metadata/android/id/changelogs/89.txt new file mode 100644 index 0000000..f829eda --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Buka sebagai..." kini tersedia di menu pada profil akun saat menggunakan beberapa akun +- Login sekarang ditangani di WebView dalam aplikasi +- Dukungan untuk Android 12 +- Dukungan untuk API konfigurasi instans Mastodon baru +- dan banyak perbaikan dan peningkatan kecil lainnya diff --git a/fastlane/metadata/android/id/changelogs/91.txt b/fastlane/metadata/android/id/changelogs/91.txt new file mode 100644 index 0000000..5dabc80 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Dukungan untuk tipe notifikasi baru pada Mastodon 3.5 +- Lencana bot sekarang terlihat lebih baik dan menyesuaikan dengan tema yang dipilih +- Teks sekarang dapat dipilih pada tampilan detail posting +- Memperbaiki banyak bug, termasuk yang mencegah login di Android 6 dan lebih rendah diff --git a/fastlane/metadata/android/id/changelogs/94.txt b/fastlane/metadata/android/id/changelogs/94.txt new file mode 100644 index 0000000..1434ff1 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Dukungan untuk Unified Push. Untuk mengaktifkan dukungan ini, Anda harus masuk kembali ke akun Anda. +- Jumlah tanggapan terhadap sebuah posting sekarang ditunjukkan di timeline. +- Gambar sekarang dapat dengan dipotong saat menulis sebuah postingan. +- Profil sekarang menunjukkan tanggal dibuatnya. +- Saat melihat daftar, judul sekarang ditampilkan di toolbar. +- Banyak perbaikan bug +- Peningkatan terjemahan diff --git a/fastlane/metadata/android/id/changelogs/97.txt b/fastlane/metadata/android/id/changelogs/97.txt new file mode 100644 index 0000000..1c73644 --- /dev/null +++ b/fastlane/metadata/android/id/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- Ikon Aplikasi Baru oleh Dzuk https://dzuk.zone/ +- Anda sekarang dapat mengikuti hashtag. Klik pada hashtag dan kemudian pada ikon di toolbar. +- Dukungan untuk Android 13 +- Dropdown baru dalam tampilan tulis untuk mengatur bahasa posting +- Tab media di profil sekarang menghormati media sensitif dan memuat lebih lancar. +- Sekarang dimungkinkan untuk menetapkan titik fokus gambar sebelum memposting +- Opsi baru untuk menampilkan nama pengguna lengkap Anda di toolbar diff --git a/fastlane/metadata/android/id/full_description.txt b/fastlane/metadata/android/id/full_description.txt new file mode 100644 index 0000000..b662922 --- /dev/null +++ b/fastlane/metadata/android/id/full_description.txt @@ -0,0 +1,12 @@ +Tusky adalah klien ringan untuk Mastodon, server jejaring sosial gratis dan sumber terbuka. + +• Desain Material +• Sebagian besar API Mastodon diimplementasikan +• Dukungan Multi-Akun +• Tema gelap dan terang dengan kemungkinan untuk beralih otomatis berdasarkan waktu hari +• Draf - buat toot dan simpan untuk nanti +• Pilih antara gaya emoji yang berbeda +• Dioptimalkan untuk semua ukuran layar +• Sepenuhnya open-source - tidak ada dependensi non-gratis seperti layanan Google + +Untuk mempelajari lebih lanjut tentang Mastodon, kunjungi https://joinmastodon.org/ diff --git a/fastlane/metadata/android/id/short_description.txt b/fastlane/metadata/android/id/short_description.txt new file mode 100644 index 0000000..106e053 --- /dev/null +++ b/fastlane/metadata/android/id/short_description.txt @@ -0,0 +1 @@ +Klien multi akun untuk jejaring sosial Mastodon diff --git a/fastlane/metadata/android/id/title.txt b/fastlane/metadata/android/id/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/id/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/is/changelogs/58.txt b/fastlane/metadata/android/is/changelogs/58.txt new file mode 100644 index 0000000..a1454d5 --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/58.txt @@ -0,0 +1,10 @@ +Tusky útg. 6 + +- Tímalínusíur hafa verið færðar í kjörstillingar notandaaðgangs og munu samstillast við vefþjón +- Nú geturðu haft sérsniðið myllumerki sem flipa í aðalviðmóti +- Hægt er að breyta listum +- Öryggi: fjarlægður stuðningur við TLS 1.0 og TLS 1.1, bætt við stuðningi við TLS 1.3 á Android 6+ +- Semja-sýnin stingur núna upp á sérsniðnum tjáningartáknum þegar byrjað er að skrifa +- Ný stilling á þema "nota þema kerfis" +- Bætt aðgengi að tímalínu +- Tusky mun núna hunsa óþekktar tilkynningar diff --git a/fastlane/metadata/android/is/changelogs/61.txt b/fastlane/metadata/android/is/changelogs/61.txt new file mode 100644 index 0000000..214189f --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky útg7.0 + +- Stuðningur við birtingu kannana, atkvæðagreiðslu og tilkynningar vegna kannana +- Nýjir hnappar til að sía tilkynningaflipa og til að eyða öllum tilkynningunum +- Eyða og endurvinna eigin tíst +- Nýtt merki á auðkennismynd sýnir hvort aðgangur sé róbót (hægt að slökkva á þessu í kjörstillingum) +- Nýjar þýðingar: Norskt bókmál og Slóvenska. diff --git a/fastlane/metadata/android/is/changelogs/67.txt b/fastlane/metadata/android/is/changelogs/67.txt new file mode 100644 index 0000000..d355b7b --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky útg9.0 + +- Nú geturðu útbúið kannanir í Tusky +- Bætt leit +- Nýr valkostur í kjörstillingum að fletta alltaf út aðvaranir vegna efnis +- Auðkennismyndir í leiðsagnarsleða eru núna ferningslaga og rúnnaðar +- Núna er hægt að kæra notendur án þess að þeir hafi sent inn stöðufærslu +- Tusky mun núna neita að tengjast með ódulrituðum (cleartext) tengingum á Android 6+ +- Margar smærri endurbætur og lagfæringar á villum diff --git a/fastlane/metadata/android/is/changelogs/68.txt b/fastlane/metadata/android/is/changelogs/68.txt new file mode 100644 index 0000000..c182a7f --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky útg. 9.1 + +Þessi útgáfa tryggir samhæfni við Mastodon 3 og bætir afköst og stöðugleika. diff --git a/fastlane/metadata/android/is/changelogs/70.txt b/fastlane/metadata/android/is/changelogs/70.txt new file mode 100644 index 0000000..bc025f8 --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Þú getur núna bókamerkt stöðufærslur og gert lista með bókamerkjunum þínum í Tusky. +- Þú getur núna sett tíst á áætlun í Tusky. Athugaðu að tíminn sem þú velur þarf að vera í það minnsta efti 5 mínútur. +- Þú getur núna bætt listum á aðalskjáinn. +- Þú getur núna sent in hljóðskrár sem viðhengi í Tusky. + +Auk hellings af smærri lagfæringum og bætingum! diff --git a/fastlane/metadata/android/is/changelogs/72.txt b/fastlane/metadata/android/is/changelogs/72.txt new file mode 100644 index 0000000..1877365 --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky útg. 11.0 + +- Tilkynningar um nýjar fylgjendabeiðnir þegar aðgangur þinn er í lás +- Nýir eiginleikar sem hægt er að víxla af/á í kjörstillingum: + - strokur milli flipa óvirkar + - staðfesting áður en tíst er endurbirt + - birta forskoðun tengla á tímalínum +- Hægt er að þagga niður í samtölum +- Niðurstöður kannana reiknast núna út frá fjölda kjósenda en ekki atkvæðum +- Margar villur lagaðar, flestar varðandi samningu tísta +- Bættar þýðingar diff --git a/fastlane/metadata/android/is/changelogs/74.txt b/fastlane/metadata/android/is/changelogs/74.txt new file mode 100644 index 0000000..f178690 --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky útg. 12.0 + +- Bætt aðalviðmót - hægt að færa flipa neðst +- Þegar þaggað er niður í notanda er núna hægt að ákveða hvort líka sé þaggað niður í tilkynningum frá honum +- Nú er hægt að fylgjast með eins mörgum myllumerkjum og maður vill í hverjum myllumerkis-flipa +- Bett leið við að birta lýsingar á myndefni, þannig að núna virka mjög langar lýsingar + +Full breytingaskrá: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/is/changelogs/77.txt b/fastlane/metadata/android/is/changelogs/77.txt new file mode 100644 index 0000000..4ff49ab --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky útg. 13.0 + +- stuðningur við minnispunkta í sniðum (Mastodon 3.2.0 eiginleiki) +- stuðningur við tilkynningar frá stjórnendum (Mastodon 3.1.0 eiginleiki) + +- auðkennismynd úr völdum aðgangi birist núna í aðalverkfærastikunni +- smellt á birtingarnafn á tímalínu opnar núna notandasniðssíðu þess notanda + +- hellingur að villulagfæringum og minni betrumbótum +- bættar þýðingar diff --git a/fastlane/metadata/android/is/changelogs/80.txt b/fastlane/metadata/android/is/changelogs/80.txt new file mode 100644 index 0000000..34521b6 --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky útg. 14.0 + +- Fáðu tilkynningu þegar notandi sem þú fylgir birtir færslu - smelltu á bjöllutáknið í sniðinu hans! (Mastodon 3.3.0 eiginleiki) +- Gerð draga í Tusky hefur verið endurhönnuð til að verða fljótlegri, notendavænni og gallalaus. +- Bætt hefur verið við sérstökum vellíðunarham til að takmarka ákveðna eiginleika Tusky. +- Tusky getur núna hreyft sérsniðin tjáningartákn. +Full breytingaskrá: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/is/changelogs/82.txt b/fastlane/metadata/android/is/changelogs/82.txt new file mode 100644 index 0000000..de9857a --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky útg. 15.0 + +- Fylgjendabeiðnir eru núna alltaf birtar í aðalvalmyndinni. +- Val tíma áætlaðra færslna hefur verið endurhönnuð til samræmis við aðra hluta forritsins. +Full breytingaskrá: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/is/changelogs/83.txt b/fastlane/metadata/android/is/changelogs/83.txt new file mode 100644 index 0000000..d90eb92 --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky útg.15.1 + +Þessi útgáfa lagfærir hrun þegar skýringatexti er settur á myndefni diff --git a/fastlane/metadata/android/is/changelogs/87.txt b/fastlane/metadata/android/is/changelogs/87.txt new file mode 100644 index 0000000..d01b52c --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky útg.16.0 + +- Aðferðin við hleðslu tímalínu hefur verið endurskrifuð til að vera hraðari og einfaldari í viðhaldi. +- Tusky getur núna hreyft sérsniðin emoji-tákn á APNG & Animated WebP sniði. +- Mikið af smávægilegum göllum leystir +- Stuðningur við Android 11 +- Nýjar þýðingar: Skosk gelíska, Galisíska, Úkraínska +- Bættar þýðingar diff --git a/fastlane/metadata/android/is/changelogs/89.txt b/fastlane/metadata/android/is/changelogs/89.txt new file mode 100644 index 0000000..e379fc7 --- /dev/null +++ b/fastlane/metadata/android/is/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky útg.17.0 + +- "Opna sem..." er núna líka á valmyndinni í notendasniðum þegar verið er að nota marga aðganga +- Innskráning er núna meðhöndluð í WebView innan forritsins +- Stuðningur við Android 12 +- Stuðningur við API-kerfisviðmót fyrir nýja uppsetningu Mastodon-tilvika +- og mökkur af smærri endurbótum og lagfæringum diff --git a/fastlane/metadata/android/is/full_description.txt b/fastlane/metadata/android/is/full_description.txt new file mode 100644 index 0000000..170b2e3 --- /dev/null +++ b/fastlane/metadata/android/is/full_description.txt @@ -0,0 +1,12 @@ +Tusky er léttkeyrandi forrit fyrir Mastodon, frjálsa og opna samfélagsnetsþjóninn. + +• Material Design hönnun +• Flestar Mastodon API-skipanir +• Margir notendaaðgangar +• Dökk og ljós þemu með möguleika á sjálfvirkti skiptingu eftir tíma sólarhrings +• Drög - semja tíst og senda síðar +• Val milli mismunandi stíla emoji-tákna +• Bestað fyrir allar skjástærðir +• Algerlega frjáls hugbúnaður - ekki háð ófrjálsum tilföngum eins og t.d. Google-þjónustum + +Til að vita meira um Mastodon, kíktu á https://joinmastodon.org/ diff --git a/fastlane/metadata/android/is/short_description.txt b/fastlane/metadata/android/is/short_description.txt new file mode 100644 index 0000000..9cf0ad7 --- /dev/null +++ b/fastlane/metadata/android/is/short_description.txt @@ -0,0 +1 @@ +Margnotenda forrit fyrir Mastodon samfélagsnetið diff --git a/fastlane/metadata/android/is/title.txt b/fastlane/metadata/android/is/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/is/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/it/changelogs/100.txt b/fastlane/metadata/android/it/changelogs/100.txt new file mode 100644 index 0000000..1885389 --- /dev/null +++ b/fastlane/metadata/android/it/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Supporto per la modifica dei toot +- Nuova impostazione per selezionare la direzione di lettura preferita +- Anteprima dei media più grande e nuovo overlay per indicare i media con una descrizione +- È ora possibile account alle liste dal loro profilo +e molto altro diff --git a/fastlane/metadata/android/it/changelogs/58.txt b/fastlane/metadata/android/it/changelogs/58.txt new file mode 100644 index 0000000..d03eac1 --- /dev/null +++ b/fastlane/metadata/android/it/changelogs/58.txt @@ -0,0 +1,13 @@ +Tusky v6.0 + +- I filtri della timeline sono stati spostati in Preferenze Utente e si sincronizzeranno con il server +- Ora è possibile avere un hashtag personalizzato come scheda nell'interfaccia principale +- Le liste possono ora essere modificate +- Sicurezza: rimosso il supporto per TLS 1.0 e TLS 1.1.1, e aggiunto il supporto per TLS 1.3 su Android 6+. +- La vista della composizione suggerirà ora le emojis personalizzate quando si inizia a digitare +- Nuova impostazione del tema "Segui il tema del sistema" +- Migliorata accessibilità della timeline +- Tusky ignorerà le notifiche ignote e non andrà in crash +- Nuova impostazione: puoi selezionare una lingua su Tusky anche se differente da quella delle impostazioni del telefono +- Nuove traduzioni: ceco ed esperanto +- Molti altri miglioramenti e correzioni diff --git a/fastlane/metadata/android/it/changelogs/61.txt b/fastlane/metadata/android/it/changelogs/61.txt new file mode 100644 index 0000000..748d379 --- /dev/null +++ b/fastlane/metadata/android/it/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Supporto per la visualizzazione di sondaggi, voti e relative notifiche +- Nuovi bottoni per filtrare le notifiche ed eliminarle tutte +- Cancella e riscrivi i tuoi toots +- Nuovo indicatore che mostra sull'immagine del profilo se un account è un bot (può essere disattivato nelle preferenze) +- Nuove traduzioni: Norvegese Bokmål e sloveno. diff --git a/fastlane/metadata/android/it/changelogs/67.txt b/fastlane/metadata/android/it/changelogs/67.txt new file mode 100644 index 0000000..4838137 --- /dev/null +++ b/fastlane/metadata/android/it/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Ora puoi creare sondaggi da Tusky +- Ricerca migliorata +- Nuova opzione in Preferenze Account per espandere sempre i contenuti sensibili +- Gli avatar nel cassetto di navigazione ora hanno una forma quadrata arrotondata +- Ora è possibile segnalare gli utenti anche quando non hanno mai pubblicato uno stato +- Tusky ora rifiuterà di connettersi tramite connessioni in chiaro su Android 6+ +- Molti altri piccoli miglioramenti e correzioni di bug diff --git a/fastlane/metadata/android/it/changelogs/68.txt b/fastlane/metadata/android/it/changelogs/68.txt new file mode 100644 index 0000000..d029ac6 --- /dev/null +++ b/fastlane/metadata/android/it/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Questa versione assicura la compatibiliità con Mastodon 3 e migliora prestazioni e stabilità. diff --git a/fastlane/metadata/android/it/changelogs/70.txt b/fastlane/metadata/android/it/changelogs/70.txt new file mode 100644 index 0000000..e77f479 --- /dev/null +++ b/fastlane/metadata/android/it/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Ora puoi contrassegnare gli stati ed elencare i tuoi segnalibri in Tusky. +- Ora puoi programmare i tuoi toot con Tusky. Tieni presente che il tempo selezionato deve essere di almeno 5 minuti in futuro. +- Ora puoi aggiungere elenchi alla schermata principale. +- Ora puoi pubblicare allegati audio con Tusky. + +E molti altri piccoli miglioramenti e correzioni di bug! diff --git a/fastlane/metadata/android/it/changelogs/74.txt b/fastlane/metadata/android/it/changelogs/74.txt new file mode 100644 index 0000000..c04ab9c --- /dev/null +++ b/fastlane/metadata/android/it/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Interfaccia principale migliorata - ora puoi spostare le schede in basso +- Quando si disattiva l'audio di un utente, ora è possibile anche decidere se disattivare l'audio delle sue notifiche +- Ora puoi seguire tutti gli hashtag che desideri in una singola scheda hashtag +- Migliorata la modalità di visualizzazione delle descrizioni dei media in modo che funzioni anche per descrizioni molto lunghe + +Log delle modifiche completo: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/it/changelogs/77.txt b/fastlane/metadata/android/it/changelogs/77.txt new file mode 100644 index 0000000..e8ce24b --- /dev/null +++ b/fastlane/metadata/android/it/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- supporto per le note del profilo (funzionalità di Mastodon 3.2.0) +- supporto per gli annunci dell'amministratore (funzionalità di Mastodon 3.1.0) + +- l'avatar del tuo account selezionato verrà ora mostrato nella barra degli strumenti principale +- facendo clic sul nome visualizzato in una timeline si aprirà ora la pagina del profilo di quell'utente + +- molte correzioni di bug e piccoli miglioramenti +- traduzioni migliorate diff --git a/fastlane/metadata/android/it/full_description.txt b/fastlane/metadata/android/it/full_description.txt new file mode 100644 index 0000000..c803064 --- /dev/null +++ b/fastlane/metadata/android/it/full_description.txt @@ -0,0 +1,12 @@ +Tusky è un client leggero per Mastodon, un server di social network libero e open source. + +• Material Design +• Implementa la maggior parte delle API di Mastodon +• Supporto multi-account +• Tema scuro e chiaro con possibilità di modificarlo automaticamente in base all'ora del giorno +• Bozze - componi i toots e salvali per dopo +• Scegli tra diversi stili di emoji +• Ottimizzato per gli schermi di tutte le dimensioni +• Completamente open source - nessuna dipendenza non libera come i servizi di Google + +Per saperne di più su Mastodon, visita https://joinmastodon.org/ diff --git a/fastlane/metadata/android/it/short_description.txt b/fastlane/metadata/android/it/short_description.txt new file mode 100644 index 0000000..d556615 --- /dev/null +++ b/fastlane/metadata/android/it/short_description.txt @@ -0,0 +1 @@ +Un client multi account per il social network Mastodon diff --git a/fastlane/metadata/android/it/title.txt b/fastlane/metadata/android/it/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/it/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/ja/changelogs/68.txt b/fastlane/metadata/android/ja/changelogs/68.txt new file mode 100644 index 0000000..69f9f85 --- /dev/null +++ b/fastlane/metadata/android/ja/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +このリリースでは、Mastodon 3との互換性が確保され、パフォーマンスと安定性が向上しています。 diff --git a/fastlane/metadata/android/ja/changelogs/70.txt b/fastlane/metadata/android/ja/changelogs/70.txt new file mode 100644 index 0000000..f7e4212 --- /dev/null +++ b/fastlane/metadata/android/ja/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Tuskyでブックマークの登録、表示が行えるようになりました。 +- You can now schedule toots with Tusky. Note that the time you select has to be at least 5 minutes in the future. +- You can now add lists to the main screen. +- Tuskyで音声ファイルを投稿出来るようになりました。 + +その他、多くの細かな改善とバグの修正を行いました! diff --git a/fastlane/metadata/android/ja/changelogs/74.txt b/fastlane/metadata/android/ja/changelogs/74.txt new file mode 100644 index 0000000..27171d9 --- /dev/null +++ b/fastlane/metadata/android/ja/changelogs/74.txt @@ -0,0 +1,4 @@ +Tusky v12.0 + +- メインインタフェースの改善 - タブを下部に移動できるようになりました +- ユーザーをミュートする際、通知もミュートすることができるようになりました diff --git a/fastlane/metadata/android/ja/full_description.txt b/fastlane/metadata/android/ja/full_description.txt new file mode 100644 index 0000000..353726c --- /dev/null +++ b/fastlane/metadata/android/ja/full_description.txt @@ -0,0 +1,12 @@ +Tuskyは、自由でオープンソースのソーシャルネットワークサーバーであるMastodon用の動作の高速なクライアントです。 + +• マテリアルデザイン +• ほとんどのMastodon APIに対応 +• マルチアカウント対応 +• 時刻に基づいて自動変更可能なダークテーマとライトテーマ +• 下書き - トゥートを作成し、保存しておく +• 異なる絵文字形式を選択可能 +• すべての画面サイズに最適化 +• 完全にオープンソース - Googleサービスのような自由でない依存関係はありません + +Mastodonの詳細についてはこちらをご覧ください:https://joinmastodon.org/ diff --git a/fastlane/metadata/android/ja/short_description.txt b/fastlane/metadata/android/ja/short_description.txt new file mode 100644 index 0000000..df9ea70 --- /dev/null +++ b/fastlane/metadata/android/ja/short_description.txt @@ -0,0 +1 @@ +複数アカウントで利用可能なMastodonクライアント diff --git a/fastlane/metadata/android/ja/title.txt b/fastlane/metadata/android/ja/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/ja/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/kab/short_description.txt b/fastlane/metadata/android/kab/short_description.txt new file mode 100644 index 0000000..b770c59 --- /dev/null +++ b/fastlane/metadata/android/kab/short_description.txt @@ -0,0 +1 @@ +Asnas isefraken aget-imiḍanen n uẓeṭṭa n tmetti Mastodon diff --git a/fastlane/metadata/android/kab/title.txt b/fastlane/metadata/android/kab/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/kab/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/ko/changelogs/58.txt b/fastlane/metadata/android/ko/changelogs/58.txt new file mode 100644 index 0000000..3378d8f --- /dev/null +++ b/fastlane/metadata/android/ko/changelogs/58.txt @@ -0,0 +1,13 @@ +Tusky v6.0 + +- 타임라인 필터를 계정 설정으로 이동/서버와 동기화 기능 추가 +- 메인 인터페이스의 탭으로 커스텀 해시태그에 관련된 게시물을 모아보는 기능 추가 +- 리스트 수정 기능 추가 +- 보안: TLS 1.0과 TLS 1.1 지원 종료, Android 6 이상에 대해 TLS 1.3 지원 추가 +- 게시물 작성 화면에서, 수동으로 커스텀 이모지 입력 시 입력 제안 표시 기능 추가 +- 테마 설정을 시스템 기본값에 따르게 할 수 있습니다. +- 타임라인 접근성 향상 +- 알 수 없는 알림으로 인해 강제 종료되는 문제 수정 +- 설정 추가: 어플리케이션 언어를 시스템 언어 이외의 다른 언어로 설정 가능 +- 번역 추가: 체코어, 에스페란토 +- 기타 자잘한 기능 향상과 버그 수정 diff --git a/fastlane/metadata/android/ko/changelogs/61.txt b/fastlane/metadata/android/ko/changelogs/61.txt new file mode 100644 index 0000000..f91fa8e --- /dev/null +++ b/fastlane/metadata/android/ko/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- 투표 기능 부분적 지원 +- 알림 필터, 알림 지우기 버튼 추가 +- 툿 지우고 다시 쓰기 기능 추가 +- 계정 아바타에 봇 구분자 표시 기능 추가 (설정에서 비활성화 가능) +- 번역 추가: 노르웨이어(보크몰), 슬로베니아어. diff --git a/fastlane/metadata/android/ko/changelogs/67.txt b/fastlane/metadata/android/ko/changelogs/67.txt new file mode 100644 index 0000000..5bbd18f --- /dev/null +++ b/fastlane/metadata/android/ko/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- 이제 Tusky에서 투표를 만들 수 있습니다. +- 검색 기능이 개선되었습니다. +- 이제 계정 설정에서 컨텐츠 경고(CW)를 항상 연 상태로 설정할 수 있습니다. +- 네비게이션 바의 프로필 이미지가 사각형으로 바뀌었습니다. +- 이제 툿이 없는 이용자를 신고할 수 있습니다. +- 안드로이드 6 이상에서 일반 텍스트를 통한 연결을 거부하기 시작합니다. +- 그 외 자잘한 기능 개선과 버그를 해결하였습니다 diff --git a/fastlane/metadata/android/ko/full_description.txt b/fastlane/metadata/android/ko/full_description.txt new file mode 100644 index 0000000..9463bef --- /dev/null +++ b/fastlane/metadata/android/ko/full_description.txt @@ -0,0 +1,12 @@ +Tusky는, 무료 오픈 소스 SNS인 Mastodon에 접속하기 위한 가벼운 클라이언트입니다. + +• 머터리얼 디자인 +• Mastodon API을 대거 활용 +• 다중 계정 지원 +• 밝은 테마/어두운 테마 지원. 시간대에 따라 자동 변경 가능 +• 쓰던 게시물을 임시 저장 +• 여러 이모지 스타일 중에서 선호하는 모양 선택 가능 +• 다양한 화면 크기 지원 +• 100% 오픈 소스 - Google 서비스와 같은 상용 구성 요소를 배제 + +Mastodon에 대한 자세한 사항은 https://joinmastodon.org/ 를 참조하세요 diff --git a/fastlane/metadata/android/ko/short_description.txt b/fastlane/metadata/android/ko/short_description.txt new file mode 100644 index 0000000..eb93866 --- /dev/null +++ b/fastlane/metadata/android/ko/short_description.txt @@ -0,0 +1 @@ +Mastodon 유저를 위한 다중 계정 클라이언트 diff --git a/fastlane/metadata/android/ko/title.txt b/fastlane/metadata/android/ko/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/ko/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/lv/short_description.txt b/fastlane/metadata/android/lv/short_description.txt new file mode 100644 index 0000000..7a86032 --- /dev/null +++ b/fastlane/metadata/android/lv/short_description.txt @@ -0,0 +1 @@ +Vairāku kontu klients Mastodon sociālajam tīklam diff --git a/fastlane/metadata/android/lv/title.txt b/fastlane/metadata/android/lv/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/lv/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/nb-NO/changelogs/100.txt b/fastlane/metadata/android/nb-NO/changelogs/100.txt new file mode 100644 index 0000000..c3ed5c8 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Støtte for postendringer +- Ny innstilling for å styre ønsket leseretning +- Større mediaforhåndsvisninger og et nytt overlegg for å indikere media med beskrivelse +- Det er nå mulig å legge til kontoer til lister ifra dere profil +og mye mer diff --git a/fastlane/metadata/android/nb-NO/changelogs/103.txt b/fastlane/metadata/android/nb-NO/changelogs/103.txt new file mode 100644 index 0000000..73b64a8 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/103.txt @@ -0,0 +1,18 @@ +Tusky 22.0 beta 1 + +Funksjoner som + +- Se trendy emneknagger +- Endre bildebeskrivelser og fokuspunkt +- "oppdateringmeny" for tilgjengelighet +- Support Mastodon v4 filtre +- Vis detaljerte endringer når en post har blitt endret +- Mulighet til å vise poststatistikk i tidslinjen + +Fikser som: + +- Vis avspillingskontroll mens lyd blir avspillt +- Riktig postlengdekalkulasjon +- Publisér alltid bildetekster + +og mye mer diff --git a/fastlane/metadata/android/nb-NO/changelogs/104.txt b/fastlane/metadata/android/nb-NO/changelogs/104.txt new file mode 100644 index 0000000..51baf23 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/104.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 2 + +Nye fikser: + +- Forbedret fart for lasting av varler +- 0/1/1+ vises igjen for svar +- Vis filtertitler ikke filterord på filtrede poster +- Fikset en bug som ved åpning av status kunne åpne en urelatert lenke +- Vis "legg til"-knappen på riktig plass når det ikke er noen filtre +- Fikse diverse krasj diff --git a/fastlane/metadata/android/nb-NO/changelogs/105.txt b/fastlane/metadata/android/nb-NO/changelogs/105.txt new file mode 100644 index 0000000..b09a0e3 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/105.txt @@ -0,0 +1,11 @@ +Tusky 22.0 beta 3 + +Fikser som: + +- Fikset krasj ved visning av en tråd +- Fikset krasj ved behandilg av Mastodonfiltre +- Lenker i beskrivelser av følgere/følgerforespørsler er klikkbare +- Android varselsoppdateringer + - Androidvarsler for mastodonvarsler skal bare bli vist én gang + - Androidvarsler blir gruppert etter mastodonvarselstype (følger, nevnt, delt osv) + - Mulighet for savnede varsler har blitt fjernet. diff --git a/fastlane/metadata/android/nb-NO/changelogs/106.txt b/fastlane/metadata/android/nb-NO/changelogs/106.txt new file mode 100644 index 0000000..60fc5e8 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/106.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 4 + +Fikser: + +- Fikset gjentagene henting av varsler dersom flere kontoer er konfigurért diff --git a/fastlane/metadata/android/nb-NO/changelogs/107.txt b/fastlane/metadata/android/nb-NO/changelogs/107.txt new file mode 100644 index 0000000..3bf8e13 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/107.txt @@ -0,0 +1,6 @@ +Tusky 22.0 beta 5 + +Fikser: + +- Tilbakestillt APNG-bibliotek for å fikse ødelagte animerte emoji +- Lagre lokal kopi av varslingsmerker hvis serveren ikke støtter APIet diff --git a/fastlane/metadata/android/nb-NO/changelogs/108.txt b/fastlane/metadata/android/nb-NO/changelogs/108.txt new file mode 100644 index 0000000..ff5e3b5 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/108.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 6 + +Fikser: + +- Lagre leseposisjon i varslingsfanen oftere diff --git a/fastlane/metadata/android/nb-NO/changelogs/109.txt b/fastlane/metadata/android/nb-NO/changelogs/109.txt new file mode 100644 index 0000000..2f7dcc6 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/109.txt @@ -0,0 +1,9 @@ +Tusky 22.0 beta 7 + +Fikser: + +### Betydningsfulle bugfikser + +- Hent alle forblivende Mastodonvarsler når androidvarsler lages +- Hvis du klikker på "Skriv" fra et varsel ble feil konto valgt +- Sikkerstille at "Sist lest ID" er lagret till rett konto diff --git a/fastlane/metadata/android/nb-NO/changelogs/110.txt b/fastlane/metadata/android/nb-NO/changelogs/110.txt new file mode 100644 index 0000000..cae2bfe --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/110.txt @@ -0,0 +1,20 @@ +Tusky 22.0 + +Nye funksjoner: + +- Vis trendy emneknagger +- Følge nye emneknagger +- Bedre rekkefølge ved valg av språk +- Vis forskjellen mellom versoner av en tut +- Støtter nå mastodon v4 filtre +- Mulighet til å vise poststatistikk i tidslinjen +- Og mere... + +Fikser: + +- Husk valgt fane og posisjon +- Behold varsler til de er lest +- Riktig visning av mikset HTV og VTH tekst i profiler +- Riktig postlengdeberegning +- Publisér alltid bildebeskrivelser +- Og mer. diff --git a/fastlane/metadata/android/nb-NO/changelogs/111.txt b/fastlane/metadata/android/nb-NO/changelogs/111.txt new file mode 100644 index 0000000..dfa4322 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/111.txt @@ -0,0 +1,15 @@ +Tusky 23.0 beta 1 + +Nye funksjoner: + +- Ny instilling for å skalére tekst i brukergrenesnittet. + +Fikser: + +- Lagre kontoinformasjon korrekt +- "Pull" varsler på enheter som har androidversjoner <= 11 +- Unnvik androidbug der hvor tekstfelt kunne "glemme" at de kann klippe/lime +- Vise "differ" i endringshistorien blir ikke forlenget over skjermkanten +- Ikke krasj dersom tjeneren din ikke har postendringshistorie +- Legg til en "Slette"-knapp ved endring av et filter +- Vis ikke-firkantede emojier riktig diff --git a/fastlane/metadata/android/nb-NO/changelogs/58.txt b/fastlane/metadata/android/nb-NO/changelogs/58.txt new file mode 100644 index 0000000..8544669 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/58.txt @@ -0,0 +1,10 @@ +Tusky v6.0 + +- Tidslinjefiltre er flyttet til kontoinnstillinger, og vil nå synkroniseres med server +- Du kan nå legge til emneknagger som egne faner +- Lister kan nå endres +- Sikkerhet: Fjernet støtte for TLS 1.0 og TLS 1.1, og la til støtte for TLS 1.3 på Android 6+ +- Når du komponerer et toot vil det foreslås emojis mens du skriver +- Ny applikasjonstemainnstilling "Bruk systeminnstillinger" +- Forbedret tilgjengelighet i tidslinjen +- Mange andre forbedringer og feilrettinger diff --git a/fastlane/metadata/android/nb-NO/changelogs/61.txt b/fastlane/metadata/android/nb-NO/changelogs/61.txt new file mode 100644 index 0000000..d707e82 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Støtte for avstemninger. +- Nye knapper for å filtrere varslinger, og for å slette alle varslinger +- Slett og skriv dine egne toots på nytt +- Bot-kontoer er nå markert (markeringen kan skrus av) +- Nye språk: Norsk (bokmål) og slovensk. diff --git a/fastlane/metadata/android/nb-NO/changelogs/67.txt b/fastlane/metadata/android/nb-NO/changelogs/67.txt new file mode 100644 index 0000000..fd76f2f --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Du kan nå opprette avstemninger fra Tusky +- Forbedret søk +- Det er nå mulig å alltid utvide innholdsadvarsler. +- Kontobilder har alltid runde kanter +- Det er nå mulig å rapportere bruker som aldri har publisert et toot +- Tusky vil ikke lenger koble til over HTTP på Android 6 og nyere +- Flere mindre forbedringer og feilrettinger diff --git a/fastlane/metadata/android/nb-NO/changelogs/68.txt b/fastlane/metadata/android/nb-NO/changelogs/68.txt new file mode 100644 index 0000000..5cfca4a --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Denne versjonen sørger for kompatibilitet med Mastodon 3, og forbedrer ytelse og stabilitet. diff --git a/fastlane/metadata/android/nb-NO/changelogs/70.txt b/fastlane/metadata/android/nb-NO/changelogs/70.txt new file mode 100644 index 0000000..d19466d --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Du kan nå legge til statuser som bokmerker, og se bokmerkene i Tusky. +- Du kan nå planlegge et toot for publisering i framtiden. +- Du kan nå legge til lister på hovedskjermen. +- Du kan nå publisere lydvedlegg med Tusky. + +I tillegg er det mange andre mindre forbedringer og feilrettinger! diff --git a/fastlane/metadata/android/nb-NO/changelogs/72.txt b/fastlane/metadata/android/nb-NO/changelogs/72.txt new file mode 100644 index 0000000..53200d7 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Varsel om nye følgere når kontoen din er låst og nye følgere må godkjennes +- Ny funksjonalitet som kan skrus av og på i innstillinger: + - Sveiping mellom faner + - Bekreftelsesdialog før boosting av et toot + - Forhåndsvisning av linker i tidslinjer +- Samtaler kan nå bli dempet +- Resultatet av avstemninger vil bli kalkulert basert på antall personer som har stemt og ikke på antall stemmer +- Mange feilfikser, de fleste relatert til skriving av toots +- Oppdaterte oversettelser diff --git a/fastlane/metadata/android/nb-NO/changelogs/74.txt b/fastlane/metadata/android/nb-NO/changelogs/74.txt new file mode 100644 index 0000000..c380991 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Fanene kan nå flyttes til bunnen av applikasjonen +- Når en bruker dempes kan du også velge å dempe varslinger fra brukeren +- Du kan følge så mange stikkord du ønsker i en enkelt stikkordfane +- Forbedret måten mediabeskrivelser fungerer på slik at de også fungerer på veldig lange beskrivelser + +Komplett endringslogg: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/nb-NO/changelogs/77.txt b/fastlane/metadata/android/nb-NO/changelogs/77.txt new file mode 100644 index 0000000..7f72db6 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- støtte for profilnotater (Mastodon 3.2.0-funksjonalitet) +- støtte for administratorkunngjøringer (Mastodon 3.1.0-funksjonalitet) + +- avataren som tilhører valgt konto vil nå vises på hovedverktøylinjen +- trykk på en brukers visningsnavn i tidslinjen vil åpne profilen til brukeren + +- mange feilrettinger og mindre forbedringer +- forbedrede oversettelser diff --git a/fastlane/metadata/android/nb-NO/changelogs/80.txt b/fastlane/metadata/android/nb-NO/changelogs/80.txt new file mode 100644 index 0000000..8e4b859 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Mulighet for å bli varslet dersom en bruker du følger publiserer en ny toot - trykk på bjelle-ikonet på profilen deres (krever Mastodon 3.3.0) +- Ny og forbedret kladd-funksjonalitet. +- Velværemodus: Kan brukes til å begrense utvalgt funksjonalitet i Tusky. Du kan aktivere velværemodus i innstillinger. +- Støtte for animerte emojis. Dette er skrudd av som standard, men du kan skru det på i innstillinger. +- Komplett endringelogg: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/nb-NO/changelogs/82.txt b/fastlane/metadata/android/nb-NO/changelogs/82.txt new file mode 100644 index 0000000..91c6646 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Følgeforespørsler vises nå alltid i hovedmenyen +- Designet på tidsvelgeren som brukes for planlegge toots er likt som resten av appen +Full liste med endringer: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/nb-NO/changelogs/83.txt b/fastlane/metadata/android/nb-NO/changelogs/83.txt new file mode 100644 index 0000000..3d7767f --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Denne versjonen retter en programfeil ved skriving av bildetekst diff --git a/fastlane/metadata/android/nb-NO/changelogs/87.txt b/fastlane/metadata/android/nb-NO/changelogs/87.txt new file mode 100644 index 0000000..93d30fc --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- Tidslinjelogikken er skrevet om, og er nå raskere, med færre feil og enklere å vedlikeholde +- Tusky kan nå animere egendefinerte emojis i formatene APNG og Animated WebP +- Mange feilrettinger +- Støtte for Android 11 +- Nye oversettelser: Skotsk-gælisk, galisisk, ukrainsk +- Oppdaterte oversettelser diff --git a/fastlane/metadata/android/nb-NO/changelogs/89.txt b/fastlane/metadata/android/nb-NO/changelogs/89.txt new file mode 100644 index 0000000..af8e6e6 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Åpne som..." er nå også tilgjengelig i profil-menyen når flere profiler er i bruk +- Innlogging blir nå gjort ved hjelp av WebView i applikasjonen +- Støtte for Android 12 +- Støtte for Mastodons nye instanskonfigurasjons-APIet +- og mange andre små fikser og forbedringer diff --git a/fastlane/metadata/android/nb-NO/changelogs/91.txt b/fastlane/metadata/android/nb-NO/changelogs/91.txt new file mode 100644 index 0000000..fe3c8e0 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Støtte for Mastodon 3.5-varslingstyper +- Bot-symbolet ser nå bedre ut og endrer seg basert på valgt tema +- Det er nå mulig å markere tekst i skjermbildet som viser innleggsdetaljer +- Fikset flere feil, inkludert en som hindret innlogging på Android 6 og eldre versjoner diff --git a/fastlane/metadata/android/nb-NO/changelogs/94.txt b/fastlane/metadata/android/nb-NO/changelogs/94.txt new file mode 100644 index 0000000..954a801 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Støtte for Unified Push. For å aktivisere dette må du logg inne på kontoene dine på nytt. +- Antall tilbakemeldinger på et innlegg vises nå i tidslinjene. +- Bilder kan nå beskjæres når innlegget opprettes. +- Dato nå en profil ble opprettes vises. +- Visning av liste viser nå navnet på listen i verktøylinjen. +- En mengde feilfikser. +- Oppdaterte oversettelser. diff --git a/fastlane/metadata/android/nb-NO/changelogs/97.txt b/fastlane/metadata/android/nb-NO/changelogs/97.txt new file mode 100644 index 0000000..a379f94 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- Nytt applikasjonsikon av Dzuk https://dzuk.zone/ +- Du kan nå følge stikkord. Klikk på et stikkord, og deretter ikonet i verktøylinjen. +- Støtte for Android 13 +- Ny nedtrekksliste for å konfigurere hvilket språk et innlegg er skrevet på +- Media-fanen i profilvisning håndterer nå sensitivt media og laster fortere +- Det er nå mulig å sette fokuspunkt på et bilde før det publiseres +- Mulighet for å vise ditt fulle navn på verktøylinjen diff --git a/fastlane/metadata/android/nb-NO/full_description.txt b/fastlane/metadata/android/nb-NO/full_description.txt new file mode 100644 index 0000000..3b3ae50 --- /dev/null +++ b/fastlane/metadata/android/nb-NO/full_description.txt @@ -0,0 +1,12 @@ +Tusky er en lettvektsklient for Mastodon, et gratis/libre sosialt nettverk basert på åpen kildekode. + +• Material Design +• Støtter det meste av Mastodon-funksjonalitet +• Støtte for flere samtidige kontoer +• Mørkt og lyst fargetema med muligheten for å bytte automatisk på bestemte tider på døgnet +• Kladder - skriv toots, og lagre dem til senere +• Velg mellom forskjellige emoji-stiler +• Optimalisert for alle skjermstørrelser +• Fullstendig åpen kildekode - ingen lukkede avhengigheter, som Google-tjenester + +Om du vil vite mer om Mastodon, gå til https://joinmastodon.org/ diff --git a/fastlane/metadata/android/nb-NO/short_description.txt b/fastlane/metadata/android/nb-NO/short_description.txt new file mode 100644 index 0000000..90588ef --- /dev/null +++ b/fastlane/metadata/android/nb-NO/short_description.txt @@ -0,0 +1 @@ +En multikonto-klient for det sosiale nettverket Mastodon diff --git a/fastlane/metadata/android/nb-NO/title.txt b/fastlane/metadata/android/nb-NO/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/nb-NO/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/nl/changelogs/58.txt b/fastlane/metadata/android/nl/changelogs/58.txt new file mode 100644 index 0000000..59111c2 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/58.txt @@ -0,0 +1,13 @@ +Tusky v6.0 + +- Timeline filters have moved to Account Preferences and will sync with the server +- You can now have a custom hashtag as tab in the main interface +- Lists can now be edited +- Security: removed support for TLS 1.0 and TLS 1.1, and added support for TLS 1.3 on Android 6+ +- The compose view will now suggest custom emojis when starting to type +- New theme setting "follow system theme" +- Improved timeline accessibility +- Tusky will now ignore unknown notifications and no longer crash +- New setting: You can now override the system language and set a different language in Tusky +- New translations: Czech and Esperanto +- A lot of other improvements and fixes diff --git a/fastlane/metadata/android/nl/changelogs/61.txt b/fastlane/metadata/android/nl/changelogs/61.txt new file mode 100644 index 0000000..ea49846 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Support for displaying polls, voting and poll notifications +- New buttons to filter the notification tab and to delete all notifications +- delete & redraft your own toots +- new indicator that shows if an account is a bot on the profile image (can be turned off in the preferences) +- New translations: Norwegian Bokmål and Slovenian. diff --git a/fastlane/metadata/android/nl/changelogs/67.txt b/fastlane/metadata/android/nl/changelogs/67.txt new file mode 100644 index 0000000..4a130c0 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- You can now create Polls from Tusky +- Improved search +- New option in Account Preferences to always expand content warnings +- Avatars in the navigation drawer have now a rounded square shape +- It is now possible to report users even when they never posted a status +- Tusky will now refuse to connect over cleartext connections on Android 6+ +- A lot of other small improvements and bug fixes diff --git a/fastlane/metadata/android/nl/changelogs/68.txt b/fastlane/metadata/android/nl/changelogs/68.txt new file mode 100644 index 0000000..2c6e91e --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Deze versie garandeert compatibiliteit met Mastodon 3 en verbetert prestaties en stabiliteit. diff --git a/fastlane/metadata/android/nl/changelogs/70.txt b/fastlane/metadata/android/nl/changelogs/70.txt new file mode 100644 index 0000000..eb24287 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- You can now bookmark statuses & list your bookmarks in Tusky. +- You can now schedule toots with Tusky. Note that the time you select has to be at least 5 minutes in the future. +- You can now add lists to the main screen. +- You can now post audio attachments with Tusky. + +And a lot of other small improvements and bug fixes! diff --git a/fastlane/metadata/android/nl/changelogs/72.txt b/fastlane/metadata/android/nl/changelogs/72.txt new file mode 100644 index 0000000..0ba9c63 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Notifications about new follow requests when your account is locked +- New features that can be toggled on the Preferences screen: + - disable swiping between tabs + - show a confirmation dialog before boosting a toot + - show link previews in timelines +- Conversations can now be muted +- Poll results will now be calculated based on the number of voters and not on the number of total votes which makes multichoice polls easier to understand +- A lot of bugfixes, most of them related to composing toots +- Improved translations diff --git a/fastlane/metadata/android/nl/changelogs/74.txt b/fastlane/metadata/android/nl/changelogs/74.txt new file mode 100644 index 0000000..2999b8b --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Improved main interface - you can now move the tabs to the bottom +- When muting a user, you can now also decide whether to mute their notifications +- You can now follow as many hashtags as you want in one single hashtag tab +- Improved the way media descriptions are displayed so it works even for super long descriptions + +Full changelog: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/nl/changelogs/77.txt b/fastlane/metadata/android/nl/changelogs/77.txt new file mode 100644 index 0000000..bb3c966 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- support for profile notes (Mastodon 3.2.0 feature) +- support for admin announcements (Mastodon 3.1.0 feature) + +- the avatar of your selected account will now be shown in the main toolbar +- clicking the display name in a timeline will now open the profile page of that user + +- a lot of bug fixes and small improvements +- improved translations diff --git a/fastlane/metadata/android/nl/changelogs/80.txt b/fastlane/metadata/android/nl/changelogs/80.txt new file mode 100644 index 0000000..14d28f0 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Get notified when a followed user posts - click the bell icon on their profile! (Mastodon 3.3.0 feature) +- The draft feature in Tusky has been completely redesigned to be faster, more user friendly and less buggy. +- A new wellbeing mode that allows you to limit certain Tusky features has been added. +- Tusky can now animate custom emojis. +Full changelog: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/nl/changelogs/82.txt b/fastlane/metadata/android/nl/changelogs/82.txt new file mode 100644 index 0000000..6e6afdd --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Follow requests are now always shown in the main menu. +- The time picker for scheduling a post has a design consistent with the rest of the app now +Full changelog: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/nl/changelogs/83.txt b/fastlane/metadata/android/nl/changelogs/83.txt new file mode 100644 index 0000000..5eb77c2 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +This release fixes a crash when captioning images diff --git a/fastlane/metadata/android/nl/changelogs/87.txt b/fastlane/metadata/android/nl/changelogs/87.txt new file mode 100644 index 0000000..81411b4 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- The timeline loading logic has been completely rewritten in order to be faster, less buggy and easier to maintain. +- Tusky can now animate custom emojis in APNG & Animated WebP format. +- A lot of bugfixes +- Support for Android 11 +- New translations: Scottish Gaelic, Galician, Ukrainian +- Improved translations diff --git a/fastlane/metadata/android/nl/changelogs/89.txt b/fastlane/metadata/android/nl/changelogs/89.txt new file mode 100644 index 0000000..4b666c3 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Open as..." is now also available in the menu on account profiles when using multiple accounts +- Login is now handled in a WebView within the app +- Support for Android 12 +- support for the new Mastodon instance configuration API +- and a lot of other small fixes and improvements diff --git a/fastlane/metadata/android/nl/changelogs/91.txt b/fastlane/metadata/android/nl/changelogs/91.txt new file mode 100644 index 0000000..e1d9830 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Support for new Mastodon 3.5 notification types +- The bot badge now looks better and adjusts to the selected theme +- Text can now be selected on the post detail view +- Fixed a lot of bugs, including one that prevented logins on Android 6 and lower diff --git a/fastlane/metadata/android/nl/changelogs/94.txt b/fastlane/metadata/android/nl/changelogs/94.txt new file mode 100644 index 0000000..f584c02 --- /dev/null +++ b/fastlane/metadata/android/nl/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Support for Unified Push. To activate the support you will have to relogin into your accounts. +- The number of responses to a post is now indicated in timelines. +- Images can now by cropped while composing a post. +- Profiles now show the date when they were created. +- When viewing a list the title is now displayed in the toolbar. +- A lot of bugfixes +- Translation improvements diff --git a/fastlane/metadata/android/nl/full_description.txt b/fastlane/metadata/android/nl/full_description.txt new file mode 100644 index 0000000..fdebe1e --- /dev/null +++ b/fastlane/metadata/android/nl/full_description.txt @@ -0,0 +1,12 @@ +Tusky is een lichtgewicht app voor Mastodon, een vrij en open-source decentraal sociaal netwerk. + +• Material design +• Meeste Mastodon-API's worden ondersteund +• Support voor meerdere accounts +• Donker en licht thema, met de optie om automatisch tijdens zonsop- en ondergang te wisselen +• Concepten - schrijf berichten en bewaar ze voor later +• Kies tussen verschillende emojistijlen +• Geoptimaliseerd voor verschillende schermgroottes +• Volledig open-source - geen niet-vrije afhankelijkheden zoals Google-services + +Ga naar https://joinmastodon.org om meer over Mastodon te leren. diff --git a/fastlane/metadata/android/nl/short_description.txt b/fastlane/metadata/android/nl/short_description.txt new file mode 100644 index 0000000..a158d9f --- /dev/null +++ b/fastlane/metadata/android/nl/short_description.txt @@ -0,0 +1 @@ +Een multi-accountclient voor het sociale netwerk Mastodon diff --git a/fastlane/metadata/android/nl/title.txt b/fastlane/metadata/android/nl/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/nl/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/pl/changelogs/100.txt b/fastlane/metadata/android/pl/changelogs/100.txt new file mode 100644 index 0000000..623a758 --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Wsparcie dla edycji postów +- Nowe ustawienie preferowanego kierunku czytania +- Większe podglądy multimediów oraz nowa nakładka wskazująca na opis +- Możliwość dodawania kont do listy bezpośrednio z ich profilu +I więcej diff --git a/fastlane/metadata/android/pl/changelogs/58.txt b/fastlane/metadata/android/pl/changelogs/58.txt new file mode 100644 index 0000000..1a2d112 --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/58.txt @@ -0,0 +1,13 @@ +Tusky v6.0 + +- Filtry zostały przeniesione do Ustawień konta i synchronizują się z serwerem +- Można ustawić własny hashtag jako zakładkę w głównym interfejsie +- Listy mogą być edytowane +- Usunięto wsparcie dla TLS 1.0 i 1.1, dodano wsparcie dla TLS 1.3 dla Android 6+ +- Widok tworzenia wpisów sugeruje teraz niestandardowe emoji +- Nowe ustawienie motywu "Użyj motywu systemu" +- Ulepszona przystępność osi czasu +- Aplikacja ignoruje nieznane typy powiadomień oraz nie będzie się zawieszać z ich powodu +- Możliwość ustawić inny język niż systemowy +- Nowe tłumaczenia: czeski i esperanto +- Wiele innych ulepszeń i poprawek diff --git a/fastlane/metadata/android/pl/changelogs/61.txt b/fastlane/metadata/android/pl/changelogs/61.txt new file mode 100644 index 0000000..cbea28a --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Wsparcie dla wyświetlania ankiet, głosowania i powiadomień o ankietach +- Nowy przycisk dla filtrowania powiadomień i usuwania wszystkich powiadomień +- Możesz usunąć i napisać ponownie własne wpisy +- Przy ikonach kont ustawionych jako bot pojawia się mała ikonka robota (opcję można wyłączyć w preferencjach) +- Nowe tłumaczenia: Norweski Bokmål i Słoweński. diff --git a/fastlane/metadata/android/pl/changelogs/67.txt b/fastlane/metadata/android/pl/changelogs/67.txt new file mode 100644 index 0000000..8f5c772 --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Możesz teraz tworzyć ankiety z poziomu Tusky +- Ulepszone wyszukiwanie +- Nowa opcja w Ustawieniach konta która zawsze rozwija ostrzeżenia o zawartości +- Awatary w szufladzie nawigacji mają teraz kształt zaokrąglonego kwadratu +- Teraz możliwe jest zgłaszanie osób nawet wtedy, kiedy nic jeszcze nie wysyłali. +- Tusky nie pozwoli na połączenia przez cleartext na Androidzie 6+ +- Wiele innych małych ulepszeń i poprawek diff --git a/fastlane/metadata/android/pl/changelogs/68.txt b/fastlane/metadata/android/pl/changelogs/68.txt new file mode 100644 index 0000000..8a8be0b --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Ta aktualizacja zapewnia kompatybilność z Mastodonem 3 i poprawia wydajność i stabilność aplikacji. diff --git a/fastlane/metadata/android/pl/changelogs/70.txt b/fastlane/metadata/android/pl/changelogs/70.txt new file mode 100644 index 0000000..a1723c2 --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Dodano wsparcie dla zakładek. Możesz teraz dodawać wpisy do zakładek i przeglądać zakładki w Tusky. +- Możesz teraz zaplanować wpisy w Tusky. (Uwaga: wybrany czas do wysłania zaplanowanego wpisu musi wynosić przynajmniej 5 minut) +- Możesz teraz dodawać listy do ekranu głównego. +- Możesz teraz załączać pliki audio do wpisów. + +Plus wiele innych małych ulepszeń i poprawek! diff --git a/fastlane/metadata/android/pl/changelogs/72.txt b/fastlane/metadata/android/pl/changelogs/72.txt new file mode 100644 index 0000000..f7c1bbb --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Powiadomienia o nowych prośbach o obserwowanie kiedy Twoje konto jest zablokowane +- Nowe funkcje, które mogą być przełączone na ekranie Ustawień: + - wyłącz przesuwanie pomiędzy kartami + - pokaż potwierdzenie przed podbiciem + - pokaż podgląd linku na osi czasu +- Rozmowy mogą być teraz wyciszone +- Wyniki ankiet są teraz przeliczane na podstawie liczby głosujących, a nie łącznej liczby wszystkich głosów +- Dużo poprawek błędów, większość z nich dotyczy wpisów +- Poprawiono tłumaczenia diff --git a/fastlane/metadata/android/pl/changelogs/74.txt b/fastlane/metadata/android/pl/changelogs/74.txt new file mode 100644 index 0000000..3014b0e --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Ulepszony interfejs - teraz można przenieść zakładki na dół ekranu +- Podczas wyciszania użytkownika można teraz wyciszyć wysłane przez niego powiadomienia +- Można teraz obserwować kilka hashtagów w jednej zakładce +- Ulepszono sposób, w jaki są wyświetlane opisy załączników + +Pełna lista zmian: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/pl/changelogs/77.txt b/fastlane/metadata/android/pl/changelogs/77.txt new file mode 100644 index 0000000..5a63eea --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- Wsparcie dla opisów profilów (Funkcja dostępna w Mastodonie 3.2.0) +- Wsparcie dla ogłoszeń od administracji (Funkcja dostępna w Mastodonie 3.1.0) + +- Zdjęcie profilowe wybranego konta widnieje teraz w głównym pasku nawigacyjnym +- Kliknięcie nazwy użytkownika na osi czasu teraz otwiera profil tego użytkownika + +- Wiele małych poprawek +- Ulepszone tłumaczenia diff --git a/fastlane/metadata/android/pl/changelogs/80.txt b/fastlane/metadata/android/pl/changelogs/80.txt new file mode 100644 index 0000000..e4d1eea --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Otrzymaj powiadomienie gdy użytkownik, którego obserwujesz, prześle wpis - kliknij na ikonkę dzwonka na ich profilu! (Funkcja dostępna w Mastodonie 3.3.0) +- Szkice zostały przeprojektowane, by ułatwić ich używanie. +- Nowy tryb samopoczucia, który pozwala Ci limitować niektóre funkcje. +- Dodano funkcję animowania niestandardowych emoji. +Pełna lista zmian: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/pl/changelogs/82.txt b/fastlane/metadata/android/pl/changelogs/82.txt new file mode 100644 index 0000000..6329da7 --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Prośby o zezwolenie na obserwowanie są teraz wyświetlane na menu głównym +- Design wyboru czasu wysłania zaplanowanych wpisów jest bardziej zgodny z resztą aplikacji +Pełna lista zmian: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/pl/changelogs/83.txt b/fastlane/metadata/android/pl/changelogs/83.txt new file mode 100644 index 0000000..c903839 --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +To wydanie naprawia błąd aplikacji przy dodawaniu opisów do zdjęć diff --git a/fastlane/metadata/android/pl/changelogs/87.txt b/fastlane/metadata/android/pl/changelogs/87.txt new file mode 100644 index 0000000..2b8d41f --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- Logika ładowania osi czasu została przepisana w celu przyspieszenia jej i naprawienia błędów. +- Tusky wspiera teraz animowane emotikony w formatach APNG i Animated WebP. +- Mnóstwo poprawek +- Wsparcie dla Androida 11 +- Nowe tłumaczenia: Gaelicki szkocki, galicyjski, ukraiński +- Ulepszone tłumaczenia diff --git a/fastlane/metadata/android/pl/changelogs/89.txt b/fastlane/metadata/android/pl/changelogs/89.txt new file mode 100644 index 0000000..edfc6f0 --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Otwórz jako..." teraz jest także dostępne w menu na profilach kont gdy używane jest kilka kont +- Login teraz jest obsługiwany w WebView w aplikacji +- Wsparcie dla Androida 12 +- Wsparcie nowego API konfiguracji instancji Mastodon +- i wiele innych małych poprawek i ulepszeń diff --git a/fastlane/metadata/android/pl/changelogs/91.txt b/fastlane/metadata/android/pl/changelogs/91.txt new file mode 100644 index 0000000..8240960 --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Wsparcie dla nowych rodzajów powiadomień w Mastodon 3.5 +- Nowy wygląd plakietek botów dopasowujący się do wybranego motywu +- Możliwość zaznaczenia tekstu w szczegółowym widoku postów +- Wprowadzenie wielu poprawek, w tym naprawienie błędu uniemożliwjającego logowanie na urządzeniach z systemem Android 6 i niższym diff --git a/fastlane/metadata/android/pl/changelogs/94.txt b/fastlane/metadata/android/pl/changelogs/94.txt new file mode 100644 index 0000000..7a7dc8b --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Wsparcie dla Unified Push. Aby aktywować wsparcie należy zalogować się ponownie na swoje konta. +- Ilość odpowiedzi na wpis jest teraz pokazywana na osiach czasu. +- Obrazki mogą teraz być przycinane podczas tworzenia wpisu. +- Profile pokazują teraz datę, w której zostały stworzone. +- Nazwa przeglądanej listy jest teraz pokazywana na górnym pasku. +- Mnóstwo poprawek +- Ulepszone tłumaczenia diff --git a/fastlane/metadata/android/pl/changelogs/97.txt b/fastlane/metadata/android/pl/changelogs/97.txt new file mode 100644 index 0000000..666a2f6 --- /dev/null +++ b/fastlane/metadata/android/pl/changelogs/97.txt @@ -0,0 +1,8 @@ +Tusky 20.0 +- Nowa ikona aplikacji autorstwa Dzuka https://dzuk.zone/ +- Można teraz obserwować hashtagi. Kliknij na hashtag, a następnie na ikonę na pasku narzędzi. +- Wsparcie Androida 13 +- Nowe menu w widoku kompozycji, umożliwiające ustawienie języka wpisu +- Zakładka z multimediami w profilach szanuje teraz wrażliwe media i ładuje się płynniej. +- Możliwe jest teraz ustawienie punktu centralnego obrazu przed opublikowaniem wpisu +- Nowa opcja pokazywania pełnej nazwy użytkownika na pasku narzędzi diff --git a/fastlane/metadata/android/pl/full_description.txt b/fastlane/metadata/android/pl/full_description.txt new file mode 100644 index 0000000..ad6bbf5 --- /dev/null +++ b/fastlane/metadata/android/pl/full_description.txt @@ -0,0 +1,12 @@ +Tusky jest lekkim klientem dla Mastodona, darmowej i otwartoźródłowej sieci społecznościowej. + +• Material Design +• Wsparcie dla większości API Mastodona +• Wsparcie kilku kont na raz +• Ciemny i jasny motyw z możliwością automatycznej zmiany bazując na porze dnia +• Szkice - stwórz wpis i zachowaj go na później +• Wybierz między różnymi stylami emoji +• Zoptymalizowany dla wszystkich rozmiarów ekranu +• W pełni open-source - bez niewolnych elementów np. serwisów Google + +By dowiedzieć się więcej o sieci Mastodon, zobacz stronę https://joinmastodon.org/ diff --git a/fastlane/metadata/android/pl/short_description.txt b/fastlane/metadata/android/pl/short_description.txt new file mode 100644 index 0000000..5bf45c7 --- /dev/null +++ b/fastlane/metadata/android/pl/short_description.txt @@ -0,0 +1 @@ +Klient do sieci Mastodon wspierający kilka kont na raz diff --git a/fastlane/metadata/android/pl/title.txt b/fastlane/metadata/android/pl/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/pl/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/pt-BR/changelogs/100.txt b/fastlane/metadata/android/pt-BR/changelogs/100.txt new file mode 100644 index 0000000..9696450 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Suporte a edição de toots +- Nova configuração para controlar a direção de leitura preferida +- Pré-visualizações de mídia maiores e uma novo indicador de mídias com descrição +- Agora é possível adicionar contas a listas diretamente de seus perfis +e muito mais diff --git a/fastlane/metadata/android/pt-BR/changelogs/58.txt b/fastlane/metadata/android/pt-BR/changelogs/58.txt new file mode 100644 index 0000000..f365969 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/58.txt @@ -0,0 +1,12 @@ +Tusky v6.0 + +- Filtros de timeline movidos para "Preferências da conta" sincronizando com servidor +- Agora você pode ter uma hashtag personalizada como aba +- Suporte a edição de listas +- O compositor vai sugerir emojis personalizados ao digitar +- Nova configuração de tema "seguir tema do sistema" +- Acessibilidade da timeline melhorada +- Notificações desconhecidas serão ignoradas +- Nova opção: substituir o idioma do sistema por outro +- Novas traduções: Tcheco e Esperanto +- Muitas outras melhorias diff --git a/fastlane/metadata/android/pt-BR/changelogs/61.txt b/fastlane/metadata/android/pt-BR/changelogs/61.txt new file mode 100644 index 0000000..cab6f17 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Suporte para enquetes: exibição, votação e notificação +- Novos botões para filtrar notificações e excluí-las +- Exclua e rascunhe seus toots +- Novo indicador que mostra na foto de perfil se uma conta é um robô (pode ser desativado nas preferências) +- Novas traduções: Norueguês Bokmål e Esloveno. diff --git a/fastlane/metadata/android/pt-BR/changelogs/67.txt b/fastlane/metadata/android/pt-BR/changelogs/67.txt new file mode 100644 index 0000000..acd2075 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Agora você pode criar enquetes no Tusky +- Pesquisa melhorada +- Nova opção nas preferências da conta: "Sempre expandir toots com Aviso de Conteúdo" +- Avatares em formato quadrado arredondado +- Agora é possível denunciar usuários mesmo sem toots +- Tusky se recusará a se conectar através de conexões de texto claro em Android 6+ +- Muitas outras pequenas melhorias e correções de bugs diff --git a/fastlane/metadata/android/pt-BR/changelogs/68.txt b/fastlane/metadata/android/pt-BR/changelogs/68.txt new file mode 100644 index 0000000..9179211 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Esta atualização garante compatibilidade com Mastodon 3 e melhora a performance e estabilidade. diff --git a/fastlane/metadata/android/pt-BR/changelogs/70.txt b/fastlane/metadata/android/pt-BR/changelogs/70.txt new file mode 100644 index 0000000..90d2e38 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Pra quem não aguenta mais perder toots no meio dos favoritos, o Salvos chegou! +- Agora dá para agendar toots, porém é necessário agendá-los para ao menos 5 minutos depois, certo? +- Utilidade pública: Finalmente poderemos adicionar listas na barrinha do Tusky! +- Filosofou no áudio de uma conversa e quer compartilhar com o fediverso? Você já pode anexar áudios nos toots, só não se esqueça de descrevê-los! + +E muitas outras pequenas melhorias e correções de bugs! diff --git a/fastlane/metadata/android/pt-BR/changelogs/72.txt b/fastlane/metadata/android/pt-BR/changelogs/72.txt new file mode 100644 index 0000000..3923334 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/72.txt @@ -0,0 +1,10 @@ +Tusky v11.0 + +- Notificações de seguidores pendentes quando a conta está trancada! FINALMENTE, POVO! +- Novas funcionalidades na aba de preferências: + * desativar gesto que alterna entre abas + * diálogo de confirmação antes de dar boost + * mostrar ou não prévia de links nas linhas alheias +- Conversas agora podem ser silenciadas +- Enquetes agora serão calculadas por número de votantes, o que facilitará o entendimento das enquetes de múltiplas opções +- E algumas correções a mais diff --git a/fastlane/metadata/android/pt-BR/changelogs/74.txt b/fastlane/metadata/android/pt-BR/changelogs/74.txt new file mode 100644 index 0000000..a04a72f --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/74.txt @@ -0,0 +1,9 @@ +Tusky v.12.0 + +- Interface principal melhorada - agora você pode mover as abas para baixo! +- Ao silenciar um usuário, você poderá decidir se deve silenciar as notificações também +- Agora dá para acompanhar quantas hashtags quiser em uma única aba! +- A exibição da descrição de mídia foi melhorada e funcionará até com descrições super longas +Não se esqueça de descrever suas mídias para tornar o fediverso mais inclusivo! + +Changelog completo: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/77.txt b/fastlane/metadata/android/pt-BR/changelogs/77.txt new file mode 100644 index 0000000..48cb81d --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- Suporte à função de nota pessoal sobre o perfil (novidade do Mastodon 3.2.0) +- Suporte à função de Comunicados da administração (novidade do Mastodon 3.1.0) + +- O avatar de sua conta selecionada agora ficará visível no cantinho da barra de títulos +- Tocar no nome de exibição na linha do tempo abrirá o perfil em questão + +- O desenvolvedor pegou o mata-moscas e fez um monte de pequenas melhorias e correções +- Tradução atualizada diff --git a/fastlane/metadata/android/pt-BR/changelogs/80.txt b/fastlane/metadata/android/pt-BR/changelogs/80.txt new file mode 100644 index 0000000..7ffefce --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Receba notificação quando a pessoa amada tootar - toca no sininho e aproveita! (novidade do Mastodon 3.3.0) +- Rascunhos no Tusky foi completamente redesenhado e agora está mais chique! +- Foi adicionado funções de bem-estar que permite limitar certas coisinhas no Tusky. +- Tusky consegue animar os emojis personalizados +Para ver mais: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/82.txt b/fastlane/metadata/android/pt-BR/changelogs/82.txt new file mode 100644 index 0000000..5451dee --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Seguidores Pendentes agora ficará fixo no menu principal! +- O relógio para agendar toots ficou mais bonitinho e combina melhor com o resto do Tusky +Para ver mais: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/pt-BR/changelogs/83.txt b/fastlane/metadata/android/pt-BR/changelogs/83.txt new file mode 100644 index 0000000..e49556d --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Esta atualização corrige aquele inconveniente ao descrever imagens diff --git a/fastlane/metadata/android/pt-BR/changelogs/87.txt b/fastlane/metadata/android/pt-BR/changelogs/87.txt new file mode 100644 index 0000000..afe1849 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- A lógica de carregamento da linha do tempo foi completamente reescrita para ser mais rápida, menos bugada e mais fácil de manter. +- Tusky pode agora animar emojis personalizados no formato APNG & WebP Animated. +- Muitas correções de bugs +- Suporte para Android 11 +- Novas traduções: gaélico escocês, galego, ucraniano +- Traduções melhoradas diff --git a/fastlane/metadata/android/pt-BR/changelogs/89.txt b/fastlane/metadata/android/pt-BR/changelogs/89.txt new file mode 100644 index 0000000..2c66fa4 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Abrir como..." agora também está disponível no menu dos perfis de conta ao usar várias contas +- O login agora é feito em um WebView dentro do aplicativo +- Suporte para Android 12 +- suporte para a nova API de configuração de instância Mastodon +- e muitas outras pequenas correções e melhorias diff --git a/fastlane/metadata/android/pt-BR/changelogs/91.txt b/fastlane/metadata/android/pt-BR/changelogs/91.txt new file mode 100644 index 0000000..4b5989c --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Suporte para novos tipos de notificação Mastodon 3.5 +- O emblema do bot agora parece melhor e se ajusta ao tema selecionado +- O texto agora pode ser selecionado na exibição de detalhes do toot +- Corrigido muitos bugs, incluindo um que impedia logins no Android 6 e inferior diff --git a/fastlane/metadata/android/pt-BR/changelogs/94.txt b/fastlane/metadata/android/pt-BR/changelogs/94.txt new file mode 100644 index 0000000..fbcb507 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Suporte para Unified Push. Para ativar o suporte, você terá que fazer login novamente em suas contas. +- O número de respostas a uma postagem agora é indicado nas linhas do tempo. +- As imagens agora podem ser cortadas ao compor uma postagem. +- Os perfis agora mostram a data em que foram criados. +- Ao visualizar uma lista, o título agora é exibido na barra de ferramentas. +- Muitas correções de bugs +- Melhorias na tradução diff --git a/fastlane/metadata/android/pt-BR/changelogs/97.txt b/fastlane/metadata/android/pt-BR/changelogs/97.txt new file mode 100644 index 0000000..5a441e8 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- Novo ícone do aplicativo por Dzuk https://dzuk.zone/ +- Agora você pode seguir hashtags. Clique em uma hashtag e depois no ícone na barra de ferramentas. +- Suporte ao Android 13 +- novo menu suspenso na composição para definir o idioma do toot +- A guia de mídia nos perfis agora respeita mídia sensível e carrega de maneira mais suave. +- Agora é possível definir o ponto de foco de uma imagem antes de postar +- Nova opção para mostrar seu nome de usuário completo na barra de ferramentas diff --git a/fastlane/metadata/android/pt-BR/full_description.txt b/fastlane/metadata/android/pt-BR/full_description.txt new file mode 100644 index 0000000..5ccc774 --- /dev/null +++ b/fastlane/metadata/android/pt-BR/full_description.txt @@ -0,0 +1,12 @@ +Tusky é um leve cliente para Mastodon, um servidor de rede social de código aberto e livre. + +• Design Material +• Maioria das APIS do Mastodon implementadas +• Suporte a várias contas +• Tema diurno e noturno, com possibilidade de troca automática de acordo com o horário +• Rascunhos - Componha seus toots e salve-os para mais tarde +• Escolha entre diferentes estilos de emoji +• Otimizado para todos os tamanhos de tela +• Código totalmente aberto - Sem dependências não-livres como Google Play Services + +Para ler mais sobre Mastodon, visite https://joinmastodon.org/ diff --git a/fastlane/metadata/android/pt-BR/short_description.txt b/fastlane/metadata/android/pt-BR/short_description.txt new file mode 100644 index 0000000..38a439d --- /dev/null +++ b/fastlane/metadata/android/pt-BR/short_description.txt @@ -0,0 +1 @@ +Um cliente multi-contas para a rede social Mastodon diff --git a/fastlane/metadata/android/pt-BR/title.txt b/fastlane/metadata/android/pt-BR/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/pt-BR/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/pt-PT/changelogs/100.txt b/fastlane/metadata/android/pt-PT/changelogs/100.txt new file mode 100644 index 0000000..7559685 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Suporte para edição de publicações +- Nova opção para controlar a direção de leitura preferida +- Pré-visualização maiores para ficheiros multimédia e uma nova sobreposição para identificar ficheiros multimédia com descrição +- Já é possível adicionar contas a listas através dos perfils das contas +e muito mais diff --git a/fastlane/metadata/android/pt-PT/changelogs/103.txt b/fastlane/metadata/android/pt-PT/changelogs/103.txt new file mode 100644 index 0000000..e25cf69 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/103.txt @@ -0,0 +1,18 @@ +Tusky 22.0 beta 1 + +Funcionalidades incluem: + +- Ver hashtags em destaque +- Editar descrição e ponto de foco das imagens +- Menu de atualização para melhorar acessibilidade +- Suporte para os filtros da versão 4 do Mastodon +- Mostrar alterações detalhadas quando uma publicação é editada +- Opção para mostrar estatísticas das publicações na timeline + +Correções incluem: + +- Mostrar controlos do leitor durante reprodução +- Cálculo correto do comprimento da publicação +- Publicar sempre legendas das imagens + +e muito mais diff --git a/fastlane/metadata/android/pt-PT/changelogs/104.txt b/fastlane/metadata/android/pt-PT/changelogs/104.txt new file mode 100644 index 0000000..d0157e7 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/104.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 2 + +Correções incluem: + +- Melhoramentos na velocidade de carregamento das notificações +- Reintrodução da mostragem 0/1/1+ para respostas +- Mostrar título dos filtros, em vez das palavras-chave dos filtros, nas publicações filtradas +- Corrigido um erro que poderia abrir um link não relacionado ao abrir uma publicação +- Mostrar botão "Adicionar" no local correto quando não há filtros +- Falhas diversas corrigidas diff --git a/fastlane/metadata/android/pt-PT/changelogs/117.txt b/fastlane/metadata/android/pt-PT/changelogs/117.txt new file mode 100644 index 0000000..c0b53c2 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/117.txt @@ -0,0 +1,7 @@ +Tusky 24.1 + +- O ecrã vai permanecer ligado enquanto um vídeo é reproduzido. +- Vazamento de memória corrigido. Isto deve melhorar a estabilidade e a performance. +- Os emojis são corretamente contabilizados como um caracter quando se escreve uma publicação. +- Corrigida uma falha que fazia a aplicação fechar inesperadamente quando texto era selecionado nalguns dispositivos. +- Os ícones nos textos de ajuda das timelines vazias agora estarão sempre alinhados corretamente. diff --git a/fastlane/metadata/android/pt-PT/changelogs/58.txt b/fastlane/metadata/android/pt-PT/changelogs/58.txt new file mode 100644 index 0000000..24aad2f --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/58.txt @@ -0,0 +1,12 @@ +Tusky v6.0 + +- Os filtros de timeline passaram para "Preferências da Conta" e sincronizam com servidor +- Pode ter uma hashtag personalizada como separador +- Suporte a edição de listas +- O editor sugere emojis personalizados ao escrever +- Nova configuração: "seguir tema do sistema" +- Melhor acessibilidade da timeline +- O Tusky ignora notificações desconhecidas, deixando de crashar +- Nova opção: trocar o idioma do sistema por outro +- Novas traduções +- Muitas outras melhorias e correções diff --git a/fastlane/metadata/android/pt-PT/changelogs/61.txt b/fastlane/metadata/android/pt-PT/changelogs/61.txt new file mode 100644 index 0000000..3cc7097 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Suporte para mostragem de votações, para votação e notificação de votações +- Botões novos para filtrar notificações e excluí-las +- Exclua e rascunhe os seus toots +- Novo indicador que mostra, na foto de perfil, se uma conta é um bot (pode ser desativado nas preferências) +- Novas traduções: Norueguês, Bokmål e Esloveno. diff --git a/fastlane/metadata/android/pt-PT/changelogs/67.txt b/fastlane/metadata/android/pt-PT/changelogs/67.txt new file mode 100644 index 0000000..5d3d384 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Agora pode criar votações no Tusky +- Pesquisa melhorada +- Nova opção em "Preferências da Conta": "Expandir sempre os toots com Aviso de Conteúdo" +- Avatars em formato quadrado com cantos arredondados +- Agora é possível denunciar utilizadores, mesmo que não tenham toots +- O Tusky vai recusar a ligação através de ligações simples (não encriptadas) em Android 6+ +- Muitas outras pequenas melhorias e correções de bugs diff --git a/fastlane/metadata/android/pt-PT/changelogs/68.txt b/fastlane/metadata/android/pt-PT/changelogs/68.txt new file mode 100644 index 0000000..9179211 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Esta atualização garante compatibilidade com Mastodon 3 e melhora a performance e estabilidade. diff --git a/fastlane/metadata/android/pt-PT/changelogs/70.txt b/fastlane/metadata/android/pt-PT/changelogs/70.txt new file mode 100644 index 0000000..6ac528b --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Agora é possível adicionar toots aos favoritos e ver a lista de favoritos no Tusky. +- Já pode agendar toots, no entanto é necessário agendá-los para pelo menos 5 minutos depois do momento da escrita. +- Já pode adicionar listas na barra lateral do Tusky! +- Já pode partilhar ficheiros de som nos teus toots! + +E muitas outras pequenas melhorias e correções de bugs! diff --git a/fastlane/metadata/android/pt-PT/changelogs/72.txt b/fastlane/metadata/android/pt-PT/changelogs/72.txt new file mode 100644 index 0000000..f42b0a8 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Notificações de seguidores pendentes quando a conta está trancada! +- Novas funcionalidades nas "Preferências": + * desativação do gesto que alterna entre separadores + * diálogo de confirmação antes de dar boost + * mostragem da pré-visualização de links nas timelines +- As conversas agora podem ser silenciadas +- As votações passam a ser calculadas pelo número de votantes e não pelo número de votos +- Várias correções relacionadas com a escrita de toots + - Traduções melhoradas diff --git a/fastlane/metadata/android/pt-PT/changelogs/74.txt b/fastlane/metadata/android/pt-PT/changelogs/74.txt new file mode 100644 index 0000000..9595cb1 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v.12.0 + +- Interface principal melhorada - passa a ser possível mover os separadores para baixo! +- Ao silenciar um utilizador, pode também escolher se também pretende silenciar as notificações +- Agora dá para seguir quantas hashtags quiser num único separador! +- A exibição da descrição dos conteúdos multimédia foi melhorada para suportar descrições super longas + +Registo completo de alterações: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/pt-PT/changelogs/77.txt b/fastlane/metadata/android/pt-PT/changelogs/77.txt new file mode 100644 index 0000000..01c57b3 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- Suporte para anotações em perfis (novidade do Mastodon 3.2.0) +- Suporte para anúncios do(s) administrador(es) de instâncias (novidade do Mastodon 3.1.0) + +- O avatar da sua conta selecionada passa a ficar visível na barra de ferramentas principal (canto superior esquerdo) +- Tocar no nome de utilizador na timeline abrirá o perfil em questão + +- Várias pequenas melhorias e correções +- Traduções melhoradas diff --git a/fastlane/metadata/android/pt-PT/changelogs/80.txt b/fastlane/metadata/android/pt-PT/changelogs/80.txt new file mode 100644 index 0000000..866dc8e --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Receba notificações quando um utilizador que segue publicar um toot - basta clicar no ícone do sino (novidade do Mastodon 3.3.0) +- O suporte para rascunhos do Tusky foi reescrito para ser mais rápido, simples e menos propenso a erros. +- Foi adicionado uma funcionalidade de bem-estar, que permite limitar algumas funcionalidades no Tusky. +- O Tusky já consegue animar os emojis personalizados +Registo completo de alterações: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/pt-PT/changelogs/82.txt b/fastlane/metadata/android/pt-PT/changelogs/82.txt new file mode 100644 index 0000000..0ee9e87 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- O menu principal passa a mostrar uma opção para ver os utilizadores que pediram para o seguir! +- O relógio para agendar toots ganhou um aspeto mais consistente com o resto do Tusky +Registo completo de alterações: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/pt-PT/changelogs/83.txt b/fastlane/metadata/android/pt-PT/changelogs/83.txt new file mode 100644 index 0000000..4c71e64 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +O Tusky já não crasha ao adicionar descrição às imagens diff --git a/fastlane/metadata/android/pt-PT/changelogs/87.txt b/fastlane/metadata/android/pt-PT/changelogs/87.txt new file mode 100644 index 0000000..79a8115 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- O algoritmo de carregamento da timeline foi completamente reescrito para ser mais rápida, mais estável e mais fácil de manter. +- O Tusky passa a poder animar emojis personalizados no formato APNG & WebP Animated. +- Muitas correções de bugs +- Suporte para Android 11 +- Novas traduções: gaélico escocês, galego, ucraniano +- Traduções melhoradas diff --git a/fastlane/metadata/android/pt-PT/changelogs/89.txt b/fastlane/metadata/android/pt-PT/changelogs/89.txt new file mode 100644 index 0000000..28cebc1 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Abrir como..." está disponível no menu de perfis de contas quando estão várias contas configuradas +- O login passa a ser feito numa WebView dentro da aplicação +- Suporte para Android 12 +- Suporte para a nova API de configuração de instâncias do Mastodon +- Várias pequenas melhorias e correções diff --git a/fastlane/metadata/android/pt-PT/changelogs/91.txt b/fastlane/metadata/android/pt-PT/changelogs/91.txt new file mode 100644 index 0000000..b861640 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Suporte para o novo tipo de notificações do Mastodon 3.5 +- Agora o simbolo do bot tem melhor qualidade e ajusta-se ao tema selecionado +- Agora é possivel selecionar o texto na visualização dos detalhes da publicação +- Corrigidos muitos erros, incluindo um que impedia o login no Android 6 e anteriores diff --git a/fastlane/metadata/android/pt-PT/full_description.txt b/fastlane/metadata/android/pt-PT/full_description.txt new file mode 100644 index 0000000..52d67d8 --- /dev/null +++ b/fastlane/metadata/android/pt-PT/full_description.txt @@ -0,0 +1,12 @@ +Tusky é um cliente leve para Mastodon, um servidor de rede social de código aberto e livre. + +• Design Material +• Maioria das APIs do Mastodon implementadas +• Suporte para várias contas +• Temas diurno e noturno, com possibilidade de troca automática de acordo com o horário +• Rascunhos - Escreva os seus toots e guarde-os para mais tarde +• Escolha entre estilos diferentes de emoji +• Otimizado para todos os tamanhos de ecrã +• Código totalmente aberto, sem dependências não-livres como Google Play Services + +Para ler mais sobre o Mastodon, visite o endereço https://joinmastodon.org/ diff --git a/fastlane/metadata/android/pt-PT/short_description.txt b/fastlane/metadata/android/pt-PT/short_description.txt new file mode 100644 index 0000000..38a439d --- /dev/null +++ b/fastlane/metadata/android/pt-PT/short_description.txt @@ -0,0 +1 @@ +Um cliente multi-contas para a rede social Mastodon diff --git a/fastlane/metadata/android/pt-PT/title.txt b/fastlane/metadata/android/pt-PT/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/pt-PT/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/ru/changelogs/100.txt b/fastlane/metadata/android/ru/changelogs/100.txt new file mode 100644 index 0000000..f076e25 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Поддержка редактирования постов +- Новая настройка для управления желаемым направлением чтения +- Увеличены предпросмотр медиа и новый оверлей для указания медиа с описанием +- Теперь можно добавлять аккаунты в списки из их профиля +и многое другое diff --git a/fastlane/metadata/android/ru/changelogs/103.txt b/fastlane/metadata/android/ru/changelogs/103.txt new file mode 100644 index 0000000..de9486b --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/103.txt @@ -0,0 +1,18 @@ +Tusky 22.0 beta 1 + +Особенности включают: + +- Просмотр модных хэштегов +- Редактирование подписи изображений и точки фокусировки +- Меню "Обновить" для доступности +- Поддержка фильтров Mastodon v4 +- Показывать подробные различия при правке поста +- Возможность отображения статистики постов в ленте + +Исправления включают: + +- Показ элементов управления плеером во время воспроизведения аудио +- Корректное вычисление длины поста +- Всегда публиковать подписи к изображениям + +и многое другое diff --git a/fastlane/metadata/android/ru/changelogs/104.txt b/fastlane/metadata/android/ru/changelogs/104.txt new file mode 100644 index 0000000..a9b5161 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/104.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 2 + +Исправления включают: + +- Улучшена скорость загрузки уведомлений +- Восстановлены отображения 0/1/1+ для ответов +- Показываются названия фильтров, а не ключевые слова фильтров, в отфильтрованных постах +- Исправлена ошибка, из-за которой при открытии статуса открывалась не связанная с ним ссылка +- Отображение кнопки "Добавить" в правильном месте при отсутствии фильтров +- Исправлены различные сбои diff --git a/fastlane/metadata/android/ru/changelogs/105.txt b/fastlane/metadata/android/ru/changelogs/105.txt new file mode 100644 index 0000000..35710fa --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/105.txt @@ -0,0 +1,11 @@ +Tusky 22.0 beta 3 + +Исправления включают: + +- Исправлен сбой при просмотре темы +- Исправлена ошибка при обработке фильтров Mastodon +- Ссылки в биографиях уведомлений о запросах на следовании/подписки стали кликабельными +- Обновления уведомлений для Android +- Уведомление Android для уведомления Mastodon должно быть показано только один раз +- Уведомления Android сгруппированы по типу уведомлений Mastodon (подписался, упомянул, продвинул и т.д.) +- Устранена возможность пропуска уведомлений diff --git a/fastlane/metadata/android/ru/changelogs/106.txt b/fastlane/metadata/android/ru/changelogs/106.txt new file mode 100644 index 0000000..4f238f9 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/106.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 4 + +Исправления: + +- Исправлено повторное получение уведомлений при настройке нескольких учетных записей diff --git a/fastlane/metadata/android/ru/changelogs/107.txt b/fastlane/metadata/android/ru/changelogs/107.txt new file mode 100644 index 0000000..6fd94b8 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/107.txt @@ -0,0 +1,6 @@ +Tusky 22.0 beta 5 + +Исправления: + +- Откатились от библиотеки APNG, чтобы исправить неработающие анимированные эмодзи +- Сохранение локальной копии маркера уведомления в случае, если сервер не поддерживает API diff --git a/fastlane/metadata/android/ru/changelogs/108.txt b/fastlane/metadata/android/ru/changelogs/108.txt new file mode 100644 index 0000000..8865447 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/108.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 6 + +Исправления: + +- Чаще сохранять позицию чтения на вкладке "Уведомления" diff --git a/fastlane/metadata/android/ru/changelogs/109.txt b/fastlane/metadata/android/ru/changelogs/109.txt new file mode 100644 index 0000000..a9ca71b --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/109.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 7 + +Исправления: + + +### Значительные исправления ошибок + +- Получение всех оставшихся уведомлений Mastodon при создании уведомлений для Android +- При нажатии кнопки "Написать" в уведомлении устанавливалась неправильная учетная запись +- Убедитесь, что "идентификатор последнего прочитанного уведомления" сохраняется в правильной учетной записи diff --git a/fastlane/metadata/android/ru/changelogs/110.txt b/fastlane/metadata/android/ru/changelogs/110.txt new file mode 100644 index 0000000..61708ed --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/110.txt @@ -0,0 +1,20 @@ +Таски 22.0 + +Новые возможности: + +- Просмотр модных хэштегов +- Следите за новыми хэштегами +- Улучшенное упорядочивание при выборе языков +- Показывать разницу между версиями поста +- Поддержка фильтров Mastodon v4 +- Возможность отображения статистики постов в ленте +- И многое другое... + +Исправления: + +- Запоминание выбранной вкладки и позиции +- Сохранение уведомлений до прочтения +- Корректное отображение смешанного справа-на-лево и слева-на-право текста в профилях +- Правильный расчет длины сообщения +- Всегда публиковать подписи к изображениям +- И многое другое... diff --git a/fastlane/metadata/android/ru/changelogs/111.txt b/fastlane/metadata/android/ru/changelogs/111.txt new file mode 100644 index 0000000..8562bf5 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/111.txt @@ -0,0 +1,15 @@ +Tusky 23.0 beta 1 + +Новые возможности: + +- Новое предпочтение для масштабирования текста пользовательского интерфейса + +Исправления: + +- Корректное сохранение информации об аккаунте +- "Тянущие" уведомления на устройствах под управлением Android версии <= 11 +- Работа над ошибкой Android, когда текстовые поля могли "забыть", что их можно копировать/вставлять +- Просмотр "отличий" в истории редактирования не будет выходить за край экрана +- Не падает, если на вашем сервере нет истории редактирования сообщений +- Добавить кнопку "Удалить" при редактировании фильтра +- Корректное отображение неквадратных эмодзи diff --git a/fastlane/metadata/android/ru/changelogs/112.txt b/fastlane/metadata/android/ru/changelogs/112.txt new file mode 100644 index 0000000..e6b723e --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/112.txt @@ -0,0 +1,6 @@ +Tusky 23.0 beta 2 + +Исправления: + +- Возможный сбой при редактировании полей профиля +- Увеличенное контекстное меню при редактировании описаний изображений diff --git a/fastlane/metadata/android/ru/changelogs/113.txt b/fastlane/metadata/android/ru/changelogs/113.txt new file mode 100644 index 0000000..86e993d --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/113.txt @@ -0,0 +1,15 @@ +Таски 23.0 + +Новые возможности: + +- Новая настройка для масштабирования текста пользовательского интерфейса + +Исправления: + +- Корректное сохранение информации об аккаунте +- "Тянущие" уведомления на устройствах под управлением Android версии <= 11 +- Ошибка Android, когда текстовые поля могли "забыть", что их можно копировать/вставлять +- Просмотр изменений в истории редактирования не выходит за край экрана +- Не падает, если на вашем сервере нет истории редактирования сообщений +- Корректное отображение неквадратных эмодзи +- Возможный сбой при редактировании полей профиля diff --git a/fastlane/metadata/android/ru/changelogs/115.txt b/fastlane/metadata/android/ru/changelogs/115.txt new file mode 100644 index 0000000..9db5e55 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/115.txt @@ -0,0 +1,10 @@ +Таски 24.0 + +- Блок-кавычки и блоки кода в сообщениях теперь выглядят красивее. +- Восстановлено старое поведение вкладки уведомлений. +- Значки ролей теперь отображаются в профилях. +- Улучшен видеоплеер. Теперь можно выбирать скорость воспроизведения. +- Новая опция темы, позволяющая использовать черную тему в соответствии с дизайном системы. +- Новый вид просмотра популярных постов доступен как в меню, так и в виде пользовательской вкладки. + +А также множество других улучшений и исправлений! diff --git a/fastlane/metadata/android/ru/changelogs/58.txt b/fastlane/metadata/android/ru/changelogs/58.txt new file mode 100644 index 0000000..fbce6db --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/58.txt @@ -0,0 +1,10 @@ +Tusky v6.0 + +- Фильтры ленты перенесены в настройки учетной записи и будут синхронизироваться с сервером. +- Теперь вы можете иметь собственный хэштег-вкладку +- Списки можно редактировать +- Безопасность: удалена поддержка TLS 1.0 и TLS 1.1, добавлена поддержка TLS 1.3 на Android 6+. +- При составлении сообщения теперь предлагаются пользовательские эмодзи. +- Новая настройка темы "следовать теме системы" +- Улучшена доступность ленты +- Tusky теперь игнорирует неизвестные уведомления diff --git a/fastlane/metadata/android/ru/changelogs/61.txt b/fastlane/metadata/android/ru/changelogs/61.txt new file mode 100644 index 0000000..124bdfd --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Добавлена поддержка отображения опросов, голосования и уведомлений об опросе. +- Новые кнопки для фильтрации вкладки уведомлений и удаления всех уведомлений +- удалить и перерисовать свои собственные слова +- новый индикатор, который показывает, является ли аккаунт ботом на изображении профиля (можно отключить в настройках) +- Новые переводы: норвежский букмол и словенский. diff --git a/fastlane/metadata/android/ru/changelogs/67.txt b/fastlane/metadata/android/ru/changelogs/67.txt new file mode 100644 index 0000000..adba3ba --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Создавайте опросы прямо в приложении +- Улучшенный поиск +- Новая настройка в параметрах аккаунта для раскрытия "чувствительных" постов по умолчанию +- Аватары в левом меню теперь имеют форму закруглённого квадрата +- Отныне вы можете жаловаться на пользователей, не написавших ни единого поста +- В Android 6+ Tusky больше не будет подключаться по незащищённому соединению +- Прочие различные исправления и улучшения diff --git a/fastlane/metadata/android/ru/changelogs/68.txt b/fastlane/metadata/android/ru/changelogs/68.txt new file mode 100644 index 0000000..5c139e9 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Этот релиз обеспечивает совместимость с Mastodon 3, повышает производительность и стабильность. diff --git a/fastlane/metadata/android/ru/changelogs/70.txt b/fastlane/metadata/android/ru/changelogs/70.txt new file mode 100644 index 0000000..7b67dc6 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Теперь вы можете добавлять статусы в закладки и просматривать списки закладок в Tusky. +- Теперь вы можете отправлять отложенные посты из Tusky. Учтите, что время отправки должно быть не менее, чем на пять минут позже текущего. +- Теперь вы можете добавлять списки на главный экран. +- Теперь вы можете публиковать аудио-вложения из Tusky. + +И множество других мелких улучшений и исправлений! diff --git a/fastlane/metadata/android/ru/changelogs/72.txt b/fastlane/metadata/android/ru/changelogs/72.txt new file mode 100644 index 0000000..1bcb00d --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Уведомления о новых запросах на подписку при заблокированном аккаунте +- Новые функции, доступные для включения в настройках: + - отключение перелистывания вкладок + - запрашивать подтверждение для репостов + - показывать предпросмотр ссылок в ленте +- Возможность заглушить диалоги +- Результаты опросов теперь рассчитываются на основе числа голосов, а не общего числа проголосовавших. Опросы с множественным выбором теперь проще понять +- Множество исправлений +- Улучшения перевода diff --git a/fastlane/metadata/android/ru/changelogs/74.txt b/fastlane/metadata/android/ru/changelogs/74.txt new file mode 100644 index 0000000..1c3e4ee --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Улучшен основной интерфейс - вы теперь можете перенести вкладки вниз +- При глушении пользователя теперь можно решить, скрывать ли уведомления от него +- Можно подписываться на любое количество тегов в одной вкладке +- Улучшен способ отображения описания медиа, так что теперь это работает и с очень длиными описаниями + +Полный список изменений: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/ru/changelogs/77.txt b/fastlane/metadata/android/ru/changelogs/77.txt new file mode 100644 index 0000000..e9e81c9 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- поддержка заметок профиля (функция Mastodon 3.2.0) +- поддержка объявлений администратора (функция Mastodon 3.1.0) + +- аватар выбранной вами учётной записи теперь будет отображаться на главной панели инструментов +- щелчок по отображаемому имени на временной шкале теперь откроет страницу профиля этого пользователя + +- множество исправлений ошибок и небольших улучшений +- улучшенные переводы diff --git a/fastlane/metadata/android/ru/changelogs/80.txt b/fastlane/metadata/android/ru/changelogs/80.txt new file mode 100644 index 0000000..698ebc1 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Показ уведомлений о публикации сообщений подписанных пользователей - щёлкните значок колокольчика в их профиле! (Особенность Mastodon 3.3.0) +- Функция черновика в Tusky была полностью переработана для скорости, удобства и стабильности. +- Новый режим благополучия, ограничивающий определённые функции Tusky. +- Анимация пользовательских смайлов. +Полный список изменений: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/ru/changelogs/82.txt b/fastlane/metadata/android/ru/changelogs/82.txt new file mode 100644 index 0000000..c16d990 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Запросы на подписку теперь всегда отображаются в главном меню. +- Средство выбора времени для планирования публикации теперь имеет дизайн, совместимый с остальной частью приложения. +Полный список изменений: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/ru/changelogs/83.txt b/fastlane/metadata/android/ru/changelogs/83.txt new file mode 100644 index 0000000..4c4ee83 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +В этом выпуске исправлена ошибка при создании подписей к изображениям diff --git a/fastlane/metadata/android/ru/changelogs/87.txt b/fastlane/metadata/android/ru/changelogs/87.txt new file mode 100644 index 0000000..5a16d63 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- Логика загрузки временной шкалы была полностью переписана, чтобы быть быстрее, меньше ошибок и проще в обслуживании. +- Tusky теперь может анимировать собственные смайлики в формате APNG и Animated WebP. +- Множество исправлений +- Поддержка Android 11 +- Новые переводы: шотландский гэльский, галисийский, украинский +- Улучшенные переводы diff --git a/fastlane/metadata/android/ru/changelogs/89.txt b/fastlane/metadata/android/ru/changelogs/89.txt new file mode 100644 index 0000000..065d0ac --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/89.txt @@ -0,0 +1,7 @@ +Таски v17.0 + +- "Открыть как..." теперь также доступен в меню профилей учетных записей при использовании нескольких учетных записей +- Вход в систему теперь осуществляется в WebView внутри приложения +- Поддержка Android 12 +- поддержка нового API конфигурации экземпляров Mastodon +- и множество других мелких исправлений и улучшений diff --git a/fastlane/metadata/android/ru/changelogs/91.txt b/fastlane/metadata/android/ru/changelogs/91.txt new file mode 100644 index 0000000..1b578e7 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/91.txt @@ -0,0 +1,6 @@ +Таски v18.0 + +- Поддержка новых типов уведомлений Mastodon 3.5 +- Значок бота теперь выглядит лучше и подстраивается под выбранную тему +- Текст теперь можно выбрать в окне деталей сообщения +- Исправлено множество ошибок, включая ту, которая не позволяла войти в систему на Android 6 и ниже diff --git a/fastlane/metadata/android/ru/changelogs/94.txt b/fastlane/metadata/android/ru/changelogs/94.txt new file mode 100644 index 0000000..d931ca3 --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/94.txt @@ -0,0 +1,9 @@ +Таски 19.0 + +- Поддержка Unified Push. Чтобы активировать поддержку, вам придется заново войти в свои аккаунты. +- Количество ответов на пост теперь отображается в ленте. +- Изображения теперь можно обрезать при создании поста. +- В профилях теперь отображается дата их создания. +- При просмотре списка заголовок теперь отображается на панели инструментов. +- Множество исправлений +- Улучшения перевода diff --git a/fastlane/metadata/android/ru/changelogs/97.txt b/fastlane/metadata/android/ru/changelogs/97.txt new file mode 100644 index 0000000..08b9e6e --- /dev/null +++ b/fastlane/metadata/android/ru/changelogs/97.txt @@ -0,0 +1,9 @@ +Таски 20.0 + +- Новая иконка приложения от Dzuk https://dzuk.zone/ +- Теперь вы можете следить за хэштегами. Нажмите на хэштег, а затем на иконку в панели инструментов. +- Поддержка Android 13 +- новый выпадающий список в окне создания сообщения для установки языка сообщения +- Вкладка медиа в профилях теперь учитывает чувствительные медиа и загружается более плавно. +- Теперь можно установить точку фокусировки изображения перед публикацией +- Новая опция отображения полного имени пользователя на панели инструментов diff --git a/fastlane/metadata/android/ru/full_description.txt b/fastlane/metadata/android/ru/full_description.txt new file mode 100644 index 0000000..c07f404 --- /dev/null +++ b/fastlane/metadata/android/ru/full_description.txt @@ -0,0 +1,12 @@ +Tusky – это легковесный клиент для Mastodon, свободной социальной сети с открытым кодом сервера. + +• Дизайн в стиле Material +• Поддержка большинства возможностей Mastodon +• Поддержка нескольких аккаунтов одновременно +• Тёмная и светлая темы с возможностью автопереключения в зависимости от времени суток +• Черновики: начните создавать пост и сохраните его на потом +• Выбор между различными наборами эмодзи +• Приложение оптимизировано под различные размеры экрана +• Открытый исходный код, никаких проприетарных компонентов, вроде сервисов Google. + +Чтобы узнать больше о Mastodon, перейдите на https://joinmastodon.org/ diff --git a/fastlane/metadata/android/ru/short_description.txt b/fastlane/metadata/android/ru/short_description.txt new file mode 100644 index 0000000..da535fd --- /dev/null +++ b/fastlane/metadata/android/ru/short_description.txt @@ -0,0 +1 @@ +Клиент для социальной сети Mastodon с поддержкой нескольких аккаунтов diff --git a/fastlane/metadata/android/ru/title.txt b/fastlane/metadata/android/ru/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/ru/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/sa/changelogs/72.txt b/fastlane/metadata/android/sa/changelogs/72.txt new file mode 100644 index 0000000..852fb2e --- /dev/null +++ b/fastlane/metadata/android/sa/changelogs/72.txt @@ -0,0 +1 @@ +टस्की v११।० diff --git a/fastlane/metadata/android/sa/changelogs/74.txt b/fastlane/metadata/android/sa/changelogs/74.txt new file mode 100644 index 0000000..3c0cd4c --- /dev/null +++ b/fastlane/metadata/android/sa/changelogs/74.txt @@ -0,0 +1,8 @@ +टस्की v१२.० + +- संशोधितमुख्यमाध्यमः - पीठिकाः अधोऽपि स्थापयितुं शक्यते +- कस्मैचिन्मूकत्वप्रदानप्रक्रियायां, सूचनाश्च निःशब्दा भवेन्न वेति चेतुं शक्यते +- सम्प्रति नैकानि निश्रेणिचिह्नानि यथेच्छया एकस्यामेव पीठिकायां योक्तुं शक्यते +- सामग्रीविवरणविधिः संशोधितः येन दीर्घतरविवरणमपि योक्तुं शक्यते + +सर्वाणि परिवर्तनपत्राणि https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/sa/full_description.txt b/fastlane/metadata/android/sa/full_description.txt new file mode 100644 index 0000000..4f58dfd --- /dev/null +++ b/fastlane/metadata/android/sa/full_description.txt @@ -0,0 +1,12 @@ +टस्कीति लघुग्राहिका मास्टोडन् इत्यस्य कृते, यदनावृतस्रोतो निःशुल्कसामाजिकजालवितारकम् । + +* वस्तुपरिकल्पना +* अधिकांशमास्टोडोनीयाः अनुप्रयोगविधिलेखनमाध्यमाः युक्ताः +* बहुव्यक्तित्वलेखासाहाय्यम् +* अन्धकारप्रबन्धः प्रदीप्तिप्रबन्धोऽपि समयानुसारेण स्वचालितविपरिवर्तनञ्च +* विकर्षाः - दौत्यं लिखित्वा भविष्यते रक्ष्यताम् +* नैका भावचिह्नशैल्योऽवचीयन्ताम् +* सर्वाकारयुक्तेभ्यः पटलेभ्यः सरलीकृतम् +* पूर्णत्वेनाऽनावृत्तस्रोतस्तथा च न सशुल्काधीनत्वं गुगलसेवासदृशम् + +मास्टोडोन् इत्यस्य विषयेऽधिकं ज्ञातुमत्र गम्यताम् : https://joinmastodon.org/ diff --git a/fastlane/metadata/android/sa/short_description.txt b/fastlane/metadata/android/sa/short_description.txt new file mode 100644 index 0000000..3972940 --- /dev/null +++ b/fastlane/metadata/android/sa/short_description.txt @@ -0,0 +1 @@ +एका बहुग्राहिका मास्टोडोन् इति सामाजिकलसञ्जालेभ्यः diff --git a/fastlane/metadata/android/sa/title.txt b/fastlane/metadata/android/sa/title.txt new file mode 100644 index 0000000..a1a2e8d --- /dev/null +++ b/fastlane/metadata/android/sa/title.txt @@ -0,0 +1 @@ +टस्की diff --git a/fastlane/metadata/android/si/title.txt b/fastlane/metadata/android/si/title.txt new file mode 100644 index 0000000..028f5f5 --- /dev/null +++ b/fastlane/metadata/android/si/title.txt @@ -0,0 +1 @@ +ටුස්කි diff --git a/fastlane/metadata/android/sk/changelogs/67.txt b/fastlane/metadata/android/sk/changelogs/67.txt new file mode 100644 index 0000000..616ff8d --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Teraz z Tusky môžete vytvárať ankety +- Vylepšené vyhľadávanie +- Nová možnost v nastaveniach účtu umožňuje automaticky rozbaľovať varovania obsahu +- Avatary v navigačnom menu majú odteraz zaokrúhlené rohy +- Odteraz je možné nahlásiť používateľa aj keď ešte nenapísali žiadny príspevok +- Tusky bude odteraz odmietať nešifrované spojenie na Androidu 6+ +- Veľa ďalších malých vylepšení a opráv diff --git a/fastlane/metadata/android/sk/changelogs/68.txt b/fastlane/metadata/android/sk/changelogs/68.txt new file mode 100644 index 0000000..4c0e8dd --- /dev/null +++ b/fastlane/metadata/android/sk/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Toto vydanie zaisťuje kompatibilitu s Mastodon 3 a vylepšuje výkon a stabilitu. diff --git a/fastlane/metadata/android/sk/short_description.txt b/fastlane/metadata/android/sk/short_description.txt new file mode 100644 index 0000000..f57dd48 --- /dev/null +++ b/fastlane/metadata/android/sk/short_description.txt @@ -0,0 +1 @@ +Viacúčtový klient pre sociálnu sieť Mastodon diff --git a/fastlane/metadata/android/sk/title.txt b/fastlane/metadata/android/sk/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/sk/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/sl/changelogs/100.txt b/fastlane/metadata/android/sl/changelogs/100.txt new file mode 100644 index 0000000..0c1ffa2 --- /dev/null +++ b/fastlane/metadata/android/sl/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Podpira naknadno urejanje +- Nove nastavitve, ki urejajo želeno smer branja +- Večji predogledi datotek in nova prosojnica za opisovanje datotek +- Račune je zdaj mogoče dodajati na sezname iz profila +in še veliko več diff --git a/fastlane/metadata/android/sl/changelogs/103.txt b/fastlane/metadata/android/sl/changelogs/103.txt new file mode 100644 index 0000000..13ddc6c --- /dev/null +++ b/fastlane/metadata/android/sl/changelogs/103.txt @@ -0,0 +1,18 @@ +Tusky 22.0 beta 1 + +Funkcije vsebujejo: + +- Ogled aktualnih ključnikov +- Urejanje opisov datotek in osrednjih točk +- “Osveži“ meni za dostopnost +- Podpora Mastodon v4 filtrov +- Prikaz natančnih sprememb po urejanju objave +- Možnost prikaza statistike objav na časovnici + +Izboljšave vsebujejo: + +- Prikaz kontrolne plošče med zvočnim predvajanjem +- Pravilne izračune dolžine objave +- Vsakokratno objavljanje opisa datotek + +In še veliko več diff --git a/fastlane/metadata/android/sl/changelogs/104.txt b/fastlane/metadata/android/sl/changelogs/104.txt new file mode 100644 index 0000000..8cf1a69 --- /dev/null +++ b/fastlane/metadata/android/sl/changelogs/104.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 2 + +Novosti: + +- Izboljšana hitrost nalaganja obvestil +- Obnovljen prikaz 0/1/1+ za odgovore +- Prikaz filtriranih naslovo namesto ključnih besed filtrov na objavah s filtri +- Razrešitev napake, pri kateri je odpiranje statusa lahko povzročilo odprtje druge povezave +- Prikaz gumba “Dodaj“ na ustreznih mestih brez filtrov +- Razrešitev raznih napak diff --git a/fastlane/metadata/android/sl/full_description.txt b/fastlane/metadata/android/sl/full_description.txt new file mode 100644 index 0000000..a934cba --- /dev/null +++ b/fastlane/metadata/android/sl/full_description.txt @@ -0,0 +1,12 @@ +Tusky je lahek odjemalec za Mastodon, brezplačen in odprtokoden družabni omrežni strežnik + +• Načrtovanje materialov +• Vsebuje večino Mastodonovih API-jev +• Podpira več profilov +• Temna in svetla tema z možnostjo samodejnega preklapljanja glede na čas v dnevu +• Osnutki - sestavite toote in jih shranite za pozneje +• Izbira med različnimi slogi smeškotov +• Optimizirano za vse velikosti ekranov +• Popolnoma odprtokoden - brez odvisnosti od Googlovih storitev + +Za več informacij o Mastodonu obiščite https://joinmastodon.org/ diff --git a/fastlane/metadata/android/sl/short_description.txt b/fastlane/metadata/android/sl/short_description.txt new file mode 100644 index 0000000..dc94774 --- /dev/null +++ b/fastlane/metadata/android/sl/short_description.txt @@ -0,0 +1 @@ +Odjemalec z več računi za družabno omrežje Mastodon diff --git a/fastlane/metadata/android/sl/title.txt b/fastlane/metadata/android/sl/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/sl/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/sv/changelogs/100.txt b/fastlane/metadata/android/sv/changelogs/100.txt new file mode 100644 index 0000000..8c61e65 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Stöd för inläggsredigering +- Ny inställning för att styra önskad läsriktning +- Större mediaförhandsvisningar och ett nytt överlägg för att indikera media med beskrivning +– Det är nu möjligt att lägga till konton till listor från sin profil +och mycket mer diff --git a/fastlane/metadata/android/sv/changelogs/103.txt b/fastlane/metadata/android/sv/changelogs/103.txt new file mode 100644 index 0000000..9b64c3f --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/103.txt @@ -0,0 +1,18 @@ +Tusky 22.0 beta 1 + +Funktioner inklusive: + +- Se trendiga hashtaggar +- Redigera bildbeskrivningar och fokuspunkt +- "Uppdatera"-menyn för tillgänglighet +- Stöd Mastodon v4-filter +- Visa detaljerade skillnader när ett inlägg redigeras +- Möjlighet att visa inläggsstatistik i tidslinjen + +Fixar inkluderar: + +- Visa spelarkontroller under ljuduppspelning +- Korrekt beräkning av stolplängd +- Publicera alltid bildtexter + +och mycket mer diff --git a/fastlane/metadata/android/sv/changelogs/104.txt b/fastlane/metadata/android/sv/changelogs/104.txt new file mode 100644 index 0000000..7307b7b --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/104.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 2 + +Fixar inkluderar: + +- Förbättrad aviseringshastighet +- Återställ visar 0/1/1+ för svar +- Visa filtertitlar, inte filtersökord, på filtrerade inlägg +- Fixade en bugg där öppnande av en status kunde öppna en orelaterade länk +- Visa "Lägg till"-knappen på rätt plats när det inte finns några filter +- Fixade diverse krascher diff --git a/fastlane/metadata/android/sv/changelogs/105.txt b/fastlane/metadata/android/sv/changelogs/105.txt new file mode 100644 index 0000000..df5478a --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/105.txt @@ -0,0 +1,11 @@ +Tusky 22.0 beta 3 + +Fixar inkluderar: + +- Fixade en krasch vid visning av en tråd +- Fixat kraschbehandling av Mastodon-filter +- Länkar i bios för meddelanden om följ/följ-förfrågningar är klickbara +- Uppdateringar av Android-aviseringar + - Android-avisering för en Mastodon-avisering bör endast visas en gång + - Android-aviseringar grupperas efter Mastodon-meddelandetyp (följ, nämn, boost, etc) + - Potentialen för saknade aviseringar har tagits bort diff --git a/fastlane/metadata/android/sv/changelogs/106.txt b/fastlane/metadata/android/sv/changelogs/106.txt new file mode 100644 index 0000000..2084297 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/106.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 4 + +Fixar: + +- Fixade upprepad hämtning av meddelanden om konfigurerad med flera konton diff --git a/fastlane/metadata/android/sv/changelogs/107.txt b/fastlane/metadata/android/sv/changelogs/107.txt new file mode 100644 index 0000000..1cdafd5 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/107.txt @@ -0,0 +1,6 @@ +Tusky 22.0 beta 5 + +Fixar: + +- Återställt APNG-bibliotek för att fixa trasiga animerade emojis +- Spara lokal kopia av meddelandemarkören om servern inte stöder API diff --git a/fastlane/metadata/android/sv/changelogs/108.txt b/fastlane/metadata/android/sv/changelogs/108.txt new file mode 100644 index 0000000..8814f36 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/108.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 6 + +Fixar: + +- Spara läsposition på Aviseringar fliken oftare diff --git a/fastlane/metadata/android/sv/changelogs/109.txt b/fastlane/metadata/android/sv/changelogs/109.txt new file mode 100644 index 0000000..2a6f078 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/109.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 7 + +Fixar: + + +### Betydande buggfixar + +- Hämta alla utestående Mastodon aviseringar när du skapar Android-aviseringar +- Om du klickar på "Skriv" från ett meddelande ställs fel konto in +- Se till att "senast lästa meddelande-ID" är sparat på rätt konto diff --git a/fastlane/metadata/android/sv/changelogs/110.txt b/fastlane/metadata/android/sv/changelogs/110.txt new file mode 100644 index 0000000..3e80f86 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/110.txt @@ -0,0 +1,20 @@ +Tusky 22.0 + +Nya egenskaper: + +- Se trendiga hashtaggar +- Följ nya hashtaggar +- Bättre ordning vid val av språk +- Visa skillnaden mellan versioner av ett inlägg +- Stöd Mastodon v4-filter +- Möjlighet att visa inläggsstatistik i tidslinjen +- Och mer... + +Fixar: + +- Kom ihåg vald flik och position +- Behåll aviseringar tills de läses +- Visa blandad RTL- och LTR-text korrekt i profiler +- Korrekt beräkning av stolplängd +- Publicera alltid bildtexter +- Och mer... diff --git a/fastlane/metadata/android/sv/changelogs/111.txt b/fastlane/metadata/android/sv/changelogs/111.txt new file mode 100644 index 0000000..f1ed7d0 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/111.txt @@ -0,0 +1,15 @@ +Tusky 23.0 beta 1 + +Nya funktioner: + +- Ny inställning för att skala UI-text + +Fixar: + +- Sparar kontoinformation korrekt +- "pull"-meddelanden på enheter som kör Android-versioner <= 11 +- Undvik Android-bugg där textfält kan "glömma" att de kan kopiera/klistra in +- Att visa "diffs" i redigeringshistoriken kommer inte att sträcka sig utanför skärmkanten +- Krascha inte om din server inte har någon inläggsredigeringshistorik +- Lägg till en "Radera"-knapp när du redigerar ett filter +- Visa icke-fyrkantiga emoji korrekt diff --git a/fastlane/metadata/android/sv/changelogs/112.txt b/fastlane/metadata/android/sv/changelogs/112.txt new file mode 100644 index 0000000..7a4a904 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/112.txt @@ -0,0 +1,6 @@ +Tusky 23.0 beta 2 + +Fixar: + +- Potentiell krasch vid redigering av profilfält +- Överdimensionerad snabbmeny när du redigerar bildbeskrivningar diff --git a/fastlane/metadata/android/sv/changelogs/113.txt b/fastlane/metadata/android/sv/changelogs/113.txt new file mode 100644 index 0000000..5f9cd6d --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/113.txt @@ -0,0 +1,15 @@ +Tusky 23.0 + +Nya funktioner: + +- Ny inställning för att skala UI-text + +Fixar: + +- Sparar kontoinformation korrekt +- "pull"-meddelanden på enheter som kör Android-versioner <= 11 +- Android-bugg där textfält kan "glömma" att de kan kopiera/klistra in +- Att visa ändringar i redigeringshistoriken kommer inte att sträcka sig utanför skärmkanten +- Krascha inte om din server inte har någon inläggsredigeringshistorik +- Visa icke-fyrkantiga emojis korrekt +- Potentiell krasch vid redigering av profilfält diff --git a/fastlane/metadata/android/sv/changelogs/115.txt b/fastlane/metadata/android/sv/changelogs/115.txt new file mode 100644 index 0000000..2609300 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/115.txt @@ -0,0 +1,10 @@ +Tusky 24.0 + +– Blockcitat och kodblock i inlägg ser nu snyggare ut. +- Det gamla beteendet för meddelandefliken har återställts. +- Rollbrickor visas nu på profiler. +– Videospelaren har förbättrats. Du kan nu välja uppspelningshastighet. +- Nytt temaalternativ för att använda det svarta temat när du följer systemdesignen. +- En ny vy för att se trendiga inlägg är tillgänglig både i menyn och som anpassad flik. + +Och många andra förbättringar och korrigeringar! diff --git a/fastlane/metadata/android/sv/changelogs/117.txt b/fastlane/metadata/android/sv/changelogs/117.txt new file mode 100644 index 0000000..ad3ef0c --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/117.txt @@ -0,0 +1,7 @@ +Tusky 24.1 + +- Skärmen förblir på igen medan en video spelas upp. +- En minnesläcka har åtgärdats. Detta bör förbättra stabilitet och prestanda. +- Emojis räknas nu korrekt som 1 tecken när du skriver ett inlägg. +- Fixade en krasch när text valdes på vissa enheter. +- Ikonerna i hjälptexterna för tomma tidslinjer kommer nu alltid att vara korrekt justerade. diff --git a/fastlane/metadata/android/sv/changelogs/119.txt b/fastlane/metadata/android/sv/changelogs/119.txt new file mode 100644 index 0000000..384851d --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/119.txt @@ -0,0 +1,8 @@ +Tusky 25 + +- Stöd för Mastodon översättning API +- Visa språk för inlägg +- Förbättrade skärmövergångar +- Filter inställningar har flyttats till kontoinställningar +- Poststatistik har nu stabil position +- Mycket av stabilitet och prestandaförbättringar under huven diff --git a/fastlane/metadata/android/sv/changelogs/58.txt b/fastlane/metadata/android/sv/changelogs/58.txt new file mode 100644 index 0000000..08f91a3 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/58.txt @@ -0,0 +1,13 @@ +Tusky v6.0 + +- Tidslinjefilter har flyttats till kontoinställningar och kommer att synkroniseras med servern +- Du kan nu ha en anpassad hashtagg som flik i huvudgränssnittet +- Listor kan nu redigeras +- Säkerhet: tog bort stödet för TLS 1.0 och TLS 1.1 och lade till stöd för TLS 1.3 på Android 6+ +- Skrivvyn kommer nu att föreslå anpassade emojis när du börjar skriva +- Ny temainställning "följ systemtema" +- Förbättrad tillgänglighet till tidslinjen +- Tusky kommer nu att ignorera okända meddelanden och inte längre krascha +- Ny inställning: Du kan nu åsidosätta systemspråket och ställa in ett annat språk i Tusky +- Nya översättningar: tjeckiska och esperanto +– Många andra förbättringar och fixar diff --git a/fastlane/metadata/android/sv/changelogs/61.txt b/fastlane/metadata/android/sv/changelogs/61.txt new file mode 100644 index 0000000..5171bad --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Stöd för att visa undersökningar, rösta och notifikationer +- Nya knappar för ett filtrera notifikationsfliken och att ta bort alla notifieringar +- Radera & skriv ny toot +- Ny indikator som visar om kontot är en bot vid profilbilden (kan stängas av under inställningar) +- Nya översättningar: Norska (Bokmål) and Slovenska. diff --git a/fastlane/metadata/android/sv/changelogs/67.txt b/fastlane/metadata/android/sv/changelogs/67.txt new file mode 100644 index 0000000..cf8935b --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Du kan nu skapa omröstningar från Tusky +- Förbättrad sökning +- Nytt alternativ i Kontoinställningar för att alltid utöka innehållsvarningar +- Avatarer i navigeringslådan har nu en rundad fyrkantig form +– Det är nu möjligt att rapportera användare även när de aldrig lagt upp en status +- Tusky kommer nu att vägra ansluta över klartextanslutningar på Android 6+ +– Många andra små förbättringar och buggfixar diff --git a/fastlane/metadata/android/sv/changelogs/68.txt b/fastlane/metadata/android/sv/changelogs/68.txt new file mode 100644 index 0000000..f10831f --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Denna release garanterar kompatibilitet med Mastodon 3 och ökar prestanda och stabilitet. diff --git a/fastlane/metadata/android/sv/changelogs/70.txt b/fastlane/metadata/android/sv/changelogs/70.txt new file mode 100644 index 0000000..463c9bb --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Du kan nu bokmärka inlägg & lista dina bokmärken i Tusky. +- Du kan nu schemalägga inlägg med Tusky. Observera att tiden du väljer måste vara minst 5 minuter framåt i tiden. +- Du kan nu fästa listor på huvudvyn. +- Du kan nu skicka ljudbilagor med Tusky. + +Och många andra mindre förbättringar och buggfixar! diff --git a/fastlane/metadata/android/sv/changelogs/72.txt b/fastlane/metadata/android/sv/changelogs/72.txt new file mode 100644 index 0000000..ae3975a --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Aviseringar om nya följningsförfrågningar när ditt konto är låst +- Nya funktioner som kan växlas på skärmen Inställningar: + - inaktivera svepning mellan flikarna + - visa en bekräftelsedialog innan du förstärker en tut + - visa länkförhandsvisningar i tidslinjer +– Konversationer kan nu stängas av +- Omröstningsresultat kommer nu att beräknas baserat på antalet väljare och inte på antalet totala röster, vilket gör flervalsundersökningar lättare att förstå +– Många buggfixar, de flesta relaterade till att komponera tutter +- Förbättrade översättningar diff --git a/fastlane/metadata/android/sv/changelogs/74.txt b/fastlane/metadata/android/sv/changelogs/74.txt new file mode 100644 index 0000000..bd34d9c --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Förbättrat gränssnitt - nu kan du flytta flikarna till botten +- Nu kan du när du tystar en användare också bestämma om du vill tysta deras aviseringar +- Nu kan du följa så många hashtaggar du vill i en enda hashtagg-flik +- Sättet mediebeskrivningar visas på har förbättrats så de fungerar även för superlånga beskrivningar + +Hela ändringsloggen: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/sv/changelogs/77.txt b/fastlane/metadata/android/sv/changelogs/77.txt new file mode 100644 index 0000000..5b4a74b --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- stöd för profilanteckningar (Mastodon 3.2.0-funktion) +- stöd för adminmeddelanden (Mastodon 3.1.0-funktion) + +- avataren för ditt valda konto kommer nu att visas i huvudverktygsfältet +- genom att klicka på visningsnamnet i en tidslinje öppnas nu användarens profilsida + +- många buggfixar och små förbättringar +- förbättrade översättningar diff --git a/fastlane/metadata/android/sv/changelogs/80.txt b/fastlane/metadata/android/sv/changelogs/80.txt new file mode 100644 index 0000000..8052c88 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Få ett meddelande när en användare gör inlägg - klicka på klockikonen på sin profil! (Mastodon 3.3.0-funktion) +- Draft-funktionen i Tusky har gjorts om helt för att vara snabbare, mer användarvänlig och mindre buggig. +- Ett nytt välmåendeläge som låter dig begränsa vissa Tusky-funktioner har lagts till. +- Tusky kan nu animera anpassade emojis. +Fullständig ändringslogg: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/sv/changelogs/82.txt b/fastlane/metadata/android/sv/changelogs/82.txt new file mode 100644 index 0000000..b6dd4ef --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Följ förfrågningar visas nu alltid i huvudmenyn. +– Tidsväljaren för att schemalägga ett inlägg har en design som överensstämmer med resten av appen nu +Fullständig ändringslogg: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/sv/changelogs/83.txt b/fastlane/metadata/android/sv/changelogs/83.txt new file mode 100644 index 0000000..4312a5d --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Den här versionen fixar en krasch vid bildtextning diff --git a/fastlane/metadata/android/sv/changelogs/87.txt b/fastlane/metadata/android/sv/changelogs/87.txt new file mode 100644 index 0000000..27ffb02 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +– Tidslinjens laddningslogik har skrivits om helt för att vara snabbare, mindre buggig och lättare att underhålla. +- Tusky kan nu animera anpassade emojis i APNG & Animated WebP-format. +- Många buggfixar +- Stöd för Android 11 +- Nya översättningar: skotsk gaeliska, galiciska, ukrainska +- Förbättrade översättningar diff --git a/fastlane/metadata/android/sv/changelogs/89.txt b/fastlane/metadata/android/sv/changelogs/89.txt new file mode 100644 index 0000000..6213dbc --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- "Öppna som..." finns nu även tillgängligt i menyn på kontoprofiler vid användning av flera konton +- Inloggning hanteras nu i en WebView i appen +- Stöd för Android 12 +- stöd för det nya Mastodon-instanskonfigurations-API:et +- och många andra småfixar och förbättringar diff --git a/fastlane/metadata/android/sv/changelogs/91.txt b/fastlane/metadata/android/sv/changelogs/91.txt new file mode 100644 index 0000000..71df4f7 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Stöd för nya Mastodon 3.5-meddelandetyper +- Bot-märket ser nu bättre ut och anpassar sig till det valda temat +- Text kan nu väljas i inläggets detaljvy +- Fixade många buggar, inklusive en som förhindrade inloggningar på Android 6 och lägre diff --git a/fastlane/metadata/android/sv/changelogs/94.txt b/fastlane/metadata/android/sv/changelogs/94.txt new file mode 100644 index 0000000..09f42ab --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Stöd för Unified Push. För att aktivera stöd behöver du logga in igen till dina konton. +- Antalet svar på ett inlägg visas nu i tidslinjer. +- Bilder kan nu beskäras medan du skriver ett inlägg. +- Profiler visar nu datum då de skapades. +- När en lista visas,syns också dess titel i verktygsraden. +- Många buggfixar +- Förbättrade översättningar diff --git a/fastlane/metadata/android/sv/changelogs/97.txt b/fastlane/metadata/android/sv/changelogs/97.txt new file mode 100644 index 0000000..bf18d81 --- /dev/null +++ b/fastlane/metadata/android/sv/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- Ny appikon av Dzuk https://dzuk.zone/ +- Du kan nu följa hashtaggar. Tryck på en hashtagg och sen på ikonen i verktygsraden +- Stöd för Android 13 +- Ny rullgardinsmeny för att ställa in vilket språk inlägget är skrivet på +- Mediafliken i profilvyn hanterar nu känsligt media och laddar snabbare +- Det går nu att sätta fokuspunkt för en bild innan den publiceras +- Nytt alternativ för att visa ditt fullständiga namn i verktygsraden diff --git a/fastlane/metadata/android/sv/full_description.txt b/fastlane/metadata/android/sv/full_description.txt new file mode 100644 index 0000000..88472cc --- /dev/null +++ b/fastlane/metadata/android/sv/full_description.txt @@ -0,0 +1,12 @@ +Tusky är en lättviktig klient för Mastodon, en fri/libre server för det sociala nätverket med öppen källkod. + + • Materialdesign + • De flesta av Mastodons API:er är implementerade + • Multi-konto stöd + • Mörkt och ljust tema med möjlighet att automatiskt växla baserat på tid på dagen + • Utkast - skapa tootar och spara dem för senare + • Välj mellan olika emoji-stilar + • Optimerat för alla skärmstorlekar + • Helt öppen källkod - inga oavhängiga beroenden som Google-tjänster + +För att lära dig mer om Mastodon, besök https://joinmastodon.org/ diff --git a/fastlane/metadata/android/sv/short_description.txt b/fastlane/metadata/android/sv/short_description.txt new file mode 100644 index 0000000..21e8da5 --- /dev/null +++ b/fastlane/metadata/android/sv/short_description.txt @@ -0,0 +1 @@ +En klient med stöd för flera konton för det sociala nätverket Mastodon diff --git a/fastlane/metadata/android/sv/title.txt b/fastlane/metadata/android/sv/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/sv/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/ta/short_description.txt b/fastlane/metadata/android/ta/short_description.txt new file mode 100644 index 0000000..ae06627 --- /dev/null +++ b/fastlane/metadata/android/ta/short_description.txt @@ -0,0 +1 @@ +மஸ்டொடான் சமூகத் தளத்திற்கான செயலி diff --git a/fastlane/metadata/android/ta/title.txt b/fastlane/metadata/android/ta/title.txt new file mode 100644 index 0000000..7b21ec1 --- /dev/null +++ b/fastlane/metadata/android/ta/title.txt @@ -0,0 +1 @@ +டஸ்க்கி diff --git a/fastlane/metadata/android/th/changelogs/72.txt b/fastlane/metadata/android/th/changelogs/72.txt new file mode 100644 index 0000000..f9cbe07 --- /dev/null +++ b/fastlane/metadata/android/th/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- การแจ้งเตือนเกี่ยวกับคำขอติดตามใหม่หากบัญชีของคุณไม่เป็นสาธารณะ(ล็อก) +- คุณสมบัติใหม่ที่สามารถ toggled ได้ในหน้าจอการตั้งค่า: + - ปิดการใช้งานการปัดระหว่างแท็บ + - แสดงข้อความยืนยันก่อนที่จะบูสต์ Toot + - แสดงตัวอย่างลิงก์ในไทม์ไลน์ +- สามารถปิดเสียงการสนทนาได้ในขณะนี้ (ไม่สนใจการส) +- ผลลัพธ์โพลจะคำนวณตามจำนวนผู้โหวตและไม่ได้อยู่ในคะแนนรวม ทำให้โพลหลายตัวเลือกง่ายต่อการเข้าใจ +- แก้ไขบั๊กจำนวนมาก ส่วนใหญ่เกี่ยวข้องกับการเขียน Toot +- ปรับปรุงการแปล diff --git a/fastlane/metadata/android/th/full_description.txt b/fastlane/metadata/android/th/full_description.txt new file mode 100644 index 0000000..f4d1e2f --- /dev/null +++ b/fastlane/metadata/android/th/full_description.txt @@ -0,0 +1,12 @@ +Tusky เป็นไคลเอ็นต์ที่มีน้ำหนักเบาสำหรับ Mastodon ซึ่งเป็นเซิร์ฟเวอร์เครือข่ายโซเชียลที่ฟรีและโอเพนซอร์ส + +• Material Design +• Most Mastodon APIs implemented +• รองรับการใช้งานแบบหลายบัญชี +• ชุดรูปแบบมืดและสว่าง สามารถสลับอัตโนมัติตามเวลาของวัน +• แบบร่าง - เขียน Toot และบันทึกไว้เพื่อใช้ในภายหลัง +• เลือกรูปแบบเอโมจิที่แตกต่างกันได้ +• ปรับให้เหมาะสมสำหรับทุกขนาดหน้าจอ +• โอเพ่นซอร์สสมบูรณ์ - ไม่มีการขึ้นต่อกันอย่างไม่อิสระ เช่น บริการ Google ฯลฯ + +หากต้องการเรียนรู้เพิ่มเติมเกี่ยวกับ Mastodon โปรดเยี่ยมชมที่ https://joinmastodon.org/ diff --git a/fastlane/metadata/android/th/short_description.txt b/fastlane/metadata/android/th/short_description.txt new file mode 100644 index 0000000..8d8a359 --- /dev/null +++ b/fastlane/metadata/android/th/short_description.txt @@ -0,0 +1 @@ +ไคลเอ็นต์แบบหลายบัญชีสำหรับเครือข่ายสังคม Mastodon diff --git a/fastlane/metadata/android/th/title.txt b/fastlane/metadata/android/th/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/th/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/tr/changelogs/100.txt b/fastlane/metadata/android/tr/changelogs/100.txt new file mode 100644 index 0000000..61e7011 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Sonradan düzenleme desteği +- Tercih edilen okuma yönünü kontrol etmek için yeni ayar +- Daha büyük medya önizlemeleri ve medyayı açıklama ile gösteren yeni bir kaplama +- Artık hesapları profillerinden listelere eklemek mümkün +ve çok daha fazlası diff --git a/fastlane/metadata/android/tr/changelogs/103.txt b/fastlane/metadata/android/tr/changelogs/103.txt new file mode 100644 index 0000000..2b9d152 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/103.txt @@ -0,0 +1,18 @@ +Tusky 22.0 beta 1 + +Dahil olan özellikler: + +- Trend etiketleri görüntüleyin +- Görüntü açıklamalarını ve odak noktasını düzenleme +- Erişilebilirlik için "Yenile" menüsü +- Mastodon v4 süzgeçlerini destekleyin +- Bir gönderi düzenlendiğinde ayrıntılı farkları gösterme +- Gönderi istatistiklerini zaman çizelgesinde gösterme seçeneği + +Düzeltmeler dahil: + +- Ses çalma sırasında oynatıcı kontrollerini gösterme +- Doğru gönderi uzunluğu hesaplaması +- Görsel başlıklarını her zaman yayınlayın + +ve çok daha fazlası diff --git a/fastlane/metadata/android/tr/changelogs/104.txt b/fastlane/metadata/android/tr/changelogs/104.txt new file mode 100644 index 0000000..6243fde --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/104.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 2 + +Düzeltmeler dahil: + +- Bildirim yükleme hızı iyileştirildi +- Yanıtlar için 0/1/1+ göstermeyi geri yükle +- Süzgeçlenen gönderilerde süzgeç anahtar kelimelerini değil, süzgeç başlıklarını gösterme +- Bir durum açıldığında ilgisiz bir bağlantının açılmasına neden olan bir hata düzeltildi +- Süzgeç olmadığında "Ekle" düğmesini doğru yerde göster +- Çeşitli çökmeler düzeltildi diff --git a/fastlane/metadata/android/tr/changelogs/105.txt b/fastlane/metadata/android/tr/changelogs/105.txt new file mode 100644 index 0000000..3ca39ce --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/105.txt @@ -0,0 +1,11 @@ +Tusky 22.0 beta 3 + +Düzeltmeler dahil: + +- Bir ileti dizisini görüntülerken oluşan çökme düzeltildi +- Mastodon süzgeçlerini işleme çökmesi düzeltildi +- Takip/takip isteği bildirimlerinin biyografilerindeki bağlantılar tıklanabilir +- Android Bildirimleri güncellemeleri + - Mastodon bildirimi için Android bildirimi yalnızca bir kez gösterilmeli + - Android bildirimleri Mastodon bildirim türüne göre gruplandırılmıştır (takip et, bahset, destekle, vb.) + - Bildirimleri kaçırma potansiyeli ortadan kaldırıldı diff --git a/fastlane/metadata/android/tr/changelogs/106.txt b/fastlane/metadata/android/tr/changelogs/106.txt new file mode 100644 index 0000000..d3de400 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/106.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 4 + +Düzeltmeler: + +- Birden fazla hesapla yapılandırıldığında bildirimlerin tekrar tekrar alınması düzeltildi diff --git a/fastlane/metadata/android/tr/changelogs/107.txt b/fastlane/metadata/android/tr/changelogs/107.txt new file mode 100644 index 0000000..071c21f --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/107.txt @@ -0,0 +1,6 @@ +Tusky 22.0 beta 5 + +Düzeltmeler: + +- Bozuk animasyonlu emojileri düzeltmek için APNG kütüphanesi geri alındı +- Sunucunun API'yi desteklememesi durumunda bildirim işaretleyicisinin yerel kopyasını kaydetme diff --git a/fastlane/metadata/android/tr/changelogs/108.txt b/fastlane/metadata/android/tr/changelogs/108.txt new file mode 100644 index 0000000..bc9f43b --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/108.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 6 + +Düzeltmeler: + +- Bildirimler sekmesindeki okuma konumunu daha sık kaydetme diff --git a/fastlane/metadata/android/tr/changelogs/109.txt b/fastlane/metadata/android/tr/changelogs/109.txt new file mode 100644 index 0000000..851da45 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/109.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 7 + +Düzeltmeler: + + +### Önemli hata düzeltmeleri + +- Android bildirimleri oluştururken tüm bekleyen Mastodon bildirimlerini getirin +- Bir bildirimden “Oluştur”a tıklandığında yanlış hesap ayarlanır +- “Son okunan bildirim kimliğinin” doğru hesaba kaydedildiğinden emin olun diff --git a/fastlane/metadata/android/tr/changelogs/110.txt b/fastlane/metadata/android/tr/changelogs/110.txt new file mode 100644 index 0000000..e75b14b --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/110.txt @@ -0,0 +1,20 @@ +Tusky 22.0 + +Yeni özellikler: + +- Trend hashtag'leri görüntüleyin +- Yeni hashtag'leri takip edin +- Dil seçerken daha iyi sıralama +- Bir gönderinin sürümleri arasındaki farkı gösterme +- Mastodon v4 filtrelerini destekleyin +- Gönderi istatistiklerini zaman çizelgesinde gösterme seçeneği +- Ve daha fazlası... + +Düzeltmeler: + +- Seçilen sekmeyi ve konumu hatırla +- Okunana kadar bildirimleri saklayın +- Profillerde karışık RTL ve LTR metinlerini doğru şekilde görüntüleme +- Doğru gönderi uzunluğu hesaplaması +- Görsel başlıklarını her zaman yayınlayın +- Ve daha fazlası... diff --git a/fastlane/metadata/android/tr/changelogs/111.txt b/fastlane/metadata/android/tr/changelogs/111.txt new file mode 100644 index 0000000..6e3261d --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/111.txt @@ -0,0 +1,15 @@ +Tusky 23.0 beta 1 + +Yeni özellikler: + +- Kullanıcı arayüzü metnini ölçeklendirmek için yeni tercih + +Düzeltmeler: + +- Hesap bilgilerini doğru şekilde kaydedin +- Android sürümleri <= 11 olan cihazlarda "çekme" bildirimleri +- Metin alanlarının kopyala/yapıştır yapabildiklerini "unutabildikleri" Android hatası üzerinde çalışın +- Düzenleme geçmişinde "farkları" görüntülemek ekran kenarından taşmayacak +- Sunucunuzun gönderi düzenleme geçmişi yoksa çökmeyin +- Bir filtreyi düzenlerken "Sil" düğmesi ekleme +- Kare olmayan emojiyi doğru gösterme diff --git a/fastlane/metadata/android/tr/changelogs/112.txt b/fastlane/metadata/android/tr/changelogs/112.txt new file mode 100644 index 0000000..2fe22f8 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/112.txt @@ -0,0 +1,6 @@ +Tusky 23.0 beta 2 + +Düzeltmeler: + +- Profil alanları düzenlenirken olası çökme +- Resim açıklamalarını düzenlerken büyük boyutlu içerik menüsü diff --git a/fastlane/metadata/android/tr/changelogs/113.txt b/fastlane/metadata/android/tr/changelogs/113.txt new file mode 100644 index 0000000..8b710b3 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/113.txt @@ -0,0 +1,15 @@ +Tusky 23.0 + +Yeni özellikler: + +- Kullanıcı arayüzü metnini ölçeklendirmek için yeni tercih + +Düzeltmeler: + +- Hesap bilgilerini doğru şekilde kaydedin +- Android sürümleri <= 11 olan cihazlarda “çekme” bildirimleri +- Metin alanlarının kopyalama/yapıştırma yapabildiklerini "unutmalarına" neden olan Android hatası +- Düzenleme geçmişindeki değişikliklerin görüntülenmesi ekran kenarından taşmayacak +- Sunucunuzun gönderi düzenleme geçmişi yoksa çökmeyin +- Kare olmayan emojiyi doğru gösterme +- Profil alanları düzenlenirken olası çökme diff --git a/fastlane/metadata/android/tr/changelogs/115.txt b/fastlane/metadata/android/tr/changelogs/115.txt new file mode 100644 index 0000000..4ff4cdf --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/115.txt @@ -0,0 +1,10 @@ +Tusky 24.0 + +- Gönderilerdeki çıkışlar ve kod blokları artık daha güzel görünüyor. +- Bildirim sekmesinin eski davranışı geri yüklendi. +- Rol rozetleri artık profillerde gösteriliyor. +- Video oynatıcı geliştirildi. Artık oynatma hızını seçebilirsiniz. +- Sistem tasarımını takip ederken siyah temayı kullanmak için yeni tema seçeneği. +- Trend gönderileri görmek için yeni bir görünüm hem menüde hem de özel sekme olarak mevcuttur. + +Daha birçok iyileştirme ve düzeltme! diff --git a/fastlane/metadata/android/tr/changelogs/117.txt b/fastlane/metadata/android/tr/changelogs/117.txt new file mode 100644 index 0000000..969fbde --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/117.txt @@ -0,0 +1,7 @@ +Tusky 24.1 + +- Bir video oynatılırken ekran tekrar açık kalacaktır. +- Bir bellek sızıntısı düzeltildi. Bu, kararlılığı ve performansı artıracaktır. +- Bir gönderi oluştururken emojiler artık doğru şekilde 1 karakter olarak sayılıyor. +- Bazı cihazlarda metin seçildiğinde oluşan çökme düzeltildi. +- Boş zaman çizelgelerinin yardım metinlerindeki simgeler artık her zaman doğru şekilde hizalanacak. diff --git a/fastlane/metadata/android/tr/changelogs/119.txt b/fastlane/metadata/android/tr/changelogs/119.txt new file mode 100644 index 0000000..d83773b --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/119.txt @@ -0,0 +1,8 @@ +Tusky 25 + +- Mastodon çeviri API'sini destekleyin +- Gönderi dilini göster +- Geliştirilmiş ekran geçişleri +- Süzgeç ayarları hesap tercihlerine taşındı +- Gönderi istatistikleri artık sabit konuma sahip +- Kaputun altında birçok kararlılık ve performans iyileştirmesi diff --git a/fastlane/metadata/android/tr/changelogs/58.txt b/fastlane/metadata/android/tr/changelogs/58.txt new file mode 100644 index 0000000..1a36033 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/58.txt @@ -0,0 +1,13 @@ +Tusky v6.0 + +- Zaman akışı filtreleri Hesap Tercihlerine taşındı ve sunucuyla eşitlendi +- Artık ana arayüzde özel etiketler sekmesi ne sahip olabilirsiniz +- Listeler artık düzenlenebilir +- Güvenlik: TLS 1.0 ve TLS 1.1 desteği kaldırıldı ve Android 6+'da TLS 1.3 desteği eklendi +- Oluşturma görünümü artık yazmaya başladığınızda özel emojiler önerecektir +- Yeni tema ayarı "sistem temasını izle" +- Geliştirilmiş zaman akışı erişilebilirliği +- Tusky artık bilinmeyen bildirimleri görmezden gelecek ve artık çökmeyecek +- Yeni ayar: Şimdi sistem dilinden faklı bir dile ayarlayabilirsiniz +- Yeni çeviriler: Çek ve Esperanto +- Diğer bir çok geliştirme ve düzeltme diff --git a/fastlane/metadata/android/tr/changelogs/61.txt b/fastlane/metadata/android/tr/changelogs/61.txt new file mode 100644 index 0000000..f120b29 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Anket, oylama ve anket bildirimlerini görüntüleme desteği +- Bildirim sekmesini filtrelemek ve tüm bildirimleri silmek için yeni düğmeler +- Tootlarınızı silin ve yeniden tasarlayın +- Bir hesabın profil görüntüsünde bot olup olmadığını gösteren yeni bir gösterge (ayarlardan devre dışı bırakılabilir) +- Yeni çeviriler: Norveççe Bokmål ve Slovence. diff --git a/fastlane/metadata/android/tr/changelogs/67.txt b/fastlane/metadata/android/tr/changelogs/67.txt new file mode 100644 index 0000000..4cfbe7d --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Artık Tusky'den Anket oluşturabilirsiniz +- Arama geliştirildi +- İçerik tercihlerini her zaman genişletmek için Hesap Tercihlerinde yeni seçenek +- Navigasyon çekmecesindeki avatarlar artık yuvarlatılmış kare şeklinde +- Artık kullanıcıları hiç durum bildirmemiş olsalar bile bildirmek mümkün. +- Tusky şimdi Android 6+ üzerindeki cleartext bağlantılarına bağlanmayı reddedecek +- Diğer birçok küçük iyileştirme ve hata düzeltmeleri diff --git a/fastlane/metadata/android/tr/changelogs/68.txt b/fastlane/metadata/android/tr/changelogs/68.txt new file mode 100644 index 0000000..275542e --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Bu sürüm, Mastodon 3 ile uyumluluğu sağlar ve performansı ve kararlılığı artırır. diff --git a/fastlane/metadata/android/tr/changelogs/70.txt b/fastlane/metadata/android/tr/changelogs/70.txt new file mode 100644 index 0000000..3f03c78 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Artık durumları yer imlerine ekleyebilir ve yer imlerinizi Tusky'de listeleyebilirsiniz. +- Artık Tusky ile tootları planlayabilirsiniz. Seçtiğiniz zamanın gelecekte en az 5 dakika olması gerektiğini unutmayın. +- Artık ana ekrana listeler ekleyebilirsiniz. +- Artık Tusky ile ses ekleri gönderebilirsiniz. + +Ve diğer birçok küçük iyileştirme ve hata düzeltmesi! diff --git a/fastlane/metadata/android/tr/changelogs/72.txt b/fastlane/metadata/android/tr/changelogs/72.txt new file mode 100644 index 0000000..b41b749 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/72.txt @@ -0,0 +1,10 @@ +Tusky v11.0 + +- Hesabınız kilitlendiğinde yeni takip istekleriyle ilgili bildirimler +Tercihler ekranından değiştirilebilen yeni özellikler: + - sekmeler arası kaydırmayı kapat + - durumu boost etmeden önce onay iletişim kutusu göster + - zaman çizelgesinde bağlantı önizlemeleri göster +- Konuşmalar artık sessize alınabilir +- Sonuçlar, çok oylu anketlerin anlaşılmasını kolaylaştıran toplam oy sayısına göre değil, oy veren sayısına göre hesaplanacaktır +- Çoğu durumla ilgili hata birçok düzeltmesi diff --git a/fastlane/metadata/android/tr/changelogs/74.txt b/fastlane/metadata/android/tr/changelogs/74.txt new file mode 100644 index 0000000..d6c9b4f --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Geliştirilmiş ana arayüz - artık sekmeleri aşağıya taşıyabilirsiniz +- Bir kullanıcıyı sustururken, artık bildirimlerini devre dışı bırakıp bırakmayacağınıza da karar verebilirsiniz +- Artık tek bir hashtag sekmesinden istediğiniz kadar hashtag'i takip edebilirsiniz +- Medya açıklamalarının görüntülenme şekli iyileştirildi, böylece süper uzun açıklamalarla bile çalışıyor + +Tam değişiklik günlüğü: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/tr/changelogs/77.txt b/fastlane/metadata/android/tr/changelogs/77.txt new file mode 100644 index 0000000..05fa706 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- profil notları için destek (Mastodon 3.2.0 özelliği) +- yönetici duyuruları için destek (Mastodon 3.1.0 özelliği) + +- seçtiğiniz hesabın görseli artık ana araç çubuğunda gösterilecek +- bir zaman çizelgesindeki görünen isme tıkladığınızda artık o kullanıcının profil sayfasını açacaktır + +- bir sürü hata düzeltmesi ve küçük iyileştirmeler +- geliştirilmiş çeviriler diff --git a/fastlane/metadata/android/tr/changelogs/80.txt b/fastlane/metadata/android/tr/changelogs/80.txt new file mode 100644 index 0000000..c78f4ea --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Takip edilen bir kullanıcı ileti gönderdiğinde bildirim alın - profilindeki zil simgesine tıklayın! (Mastodon 3.3.0 feature) +- Tusky'deki taslak özelliği, daha hızlı, daha kullanıcı dostu ve daha az hatalı olacak şekilde tamamen yeniden tasarlandı. +- Belirli Tusky özelliklerini sınırlamanıza izin veren yeni bir refah modu eklendi. +- Tusky artık özel emojileri oynatabilir. +Tam değişiklik günlüğü: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/tr/changelogs/82.txt b/fastlane/metadata/android/tr/changelogs/82.txt new file mode 100644 index 0000000..834b8d6 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Takip istekleri artık her zaman ana menüde gösterilmektedir. +- Bir gönderiyi anlamak için zaman seçici, artık uygulamanın geri kalanıyla tutarlı bir tasarıma sahip +Tam değişiklik günlüğü: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/tr/changelogs/83.txt b/fastlane/metadata/android/tr/changelogs/83.txt new file mode 100644 index 0000000..8e8ccfc --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Bu sürüm, resimlere başlık eklenirken oluşan bir çökmeyi düzeltir diff --git a/fastlane/metadata/android/tr/changelogs/87.txt b/fastlane/metadata/android/tr/changelogs/87.txt new file mode 100644 index 0000000..56c85e6 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- Zaman akışı yükleme mantığı, daha hızlı, daha az hatalı ve bakımı daha kolay olması için tamamen yeniden yazılmıştır. +- Tusky artık özel emojileri APNG ve Animasyonlu WebP formatında oynatılabilir. +- Birçok hata düzeltmesi +- Android 11 desteği +- Yeni çeviriler: Scottish Gaelic, Galician, Ukrainian +- İyileştirilmiş çeviriler diff --git a/fastlane/metadata/android/tr/changelogs/89.txt b/fastlane/metadata/android/tr/changelogs/89.txt new file mode 100644 index 0000000..5b4b08d --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +-Olarak aç..." artık birden fazla hesap kullanırken hesap profilleri menüsünde de mevcut +- Giriş artık uygulama içindeki bir web görünümünde gerçekleştiriliyor +- Android 12 desteği +- yeni Mastodon örnek yapılandırma API'sı desteği +- ve diğer birçok küçük düzeltme ve iyileştirme diff --git a/fastlane/metadata/android/tr/changelogs/91.txt b/fastlane/metadata/android/tr/changelogs/91.txt new file mode 100644 index 0000000..c1c01e6 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Yeni Mastodon 3.5 bildirim tipleri için destek +- Bot rozeti artık daha iyi görünüyor ve seçilen temaya göre ayarlanıyor +- Metin artık gönderi ayrıntısı görünümünde seçilebilir +- Android 6 ve daha düşük sürümlerde oturum açmayı engelleyen bir hata da dahil olmak üzere birçok hata düzeltildi diff --git a/fastlane/metadata/android/tr/changelogs/94.txt b/fastlane/metadata/android/tr/changelogs/94.txt new file mode 100644 index 0000000..e2dbf26 --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Birleşik Besleme için Destek. Desteği etkinleştirmek için hesaplarınıza yeniden giriş yapmanız gerekir. +- Bir gönderiye verilen yanıtların sayısı artık zaman akışında belirtilmiştir. +- Görüntüler artık bir gönderi oluştururken kırpılabilir. +- Profiller artık oluşturuldukları tarihi gösterir. +- Bir listeyi görüntülerken başlık artık araç çubuğunda görüntülenir. +- Birçok hata düzeltmesi +- Çeviri iyileştirmeleri diff --git a/fastlane/metadata/android/tr/changelogs/97.txt b/fastlane/metadata/android/tr/changelogs/97.txt new file mode 100644 index 0000000..dc7272b --- /dev/null +++ b/fastlane/metadata/android/tr/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- Dzuk'tan yeni Uygulama simgesi https://dzuk.zone/ +- Artık etiketleri takip edebilirsiniz. Bir etiket'e ve ardından araç çubuğundaki simgeye tıklayın. +- Android 13 Desteği +- bir gönderinin dilini ayarlamak için oluşturma görünümünde yeni açılır menü +- Profiller'deki medya sekmesi artık hassas medyaya uyumlu ve daha yumuşak yükleniyor. +- Göndermeden önce bir görüntünün odak noktasını ayarlamak artık mümkün +- Araç çubuğunda tam kullanıcı adınızı göstermek için yeni seçenek diff --git a/fastlane/metadata/android/tr/full_description.txt b/fastlane/metadata/android/tr/full_description.txt new file mode 100644 index 0000000..4b4377e --- /dev/null +++ b/fastlane/metadata/android/tr/full_description.txt @@ -0,0 +1,12 @@ +Tusky, ücretsiz ve açık kaynaklı bir sosyal ağ sunucusu olan Mastodon için hafif bir istemcidir. + +• Materyal Tasarım +• Çoğu Mastodon API'si uygulandı +• Çoklu hesap desteği +• Gün içinde karanlık ve aydınlık tema arasında otomatik geçiş imkanı +• Taslak - toot oluşturun ve daha sonra kullanmak üzere saklayın +• Farklı ifade stilleri arasında seçim yapma imkanı +• Tüm ekran boyutları için optimize edilmiş arayüz +• Tamamen açık kaynak - Google servisleri gibi özgür olmayan bağımlılıklar yok + +Mastodon hakkında daha fazla bilgi edinmek için https://joinmastodon.org/ adresini ziyaret edin diff --git a/fastlane/metadata/android/tr/short_description.txt b/fastlane/metadata/android/tr/short_description.txt new file mode 100644 index 0000000..0a464dd --- /dev/null +++ b/fastlane/metadata/android/tr/short_description.txt @@ -0,0 +1 @@ +Mastodon sosyal ağı için çok hesaplı bir istemci diff --git a/fastlane/metadata/android/tr/title.txt b/fastlane/metadata/android/tr/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/tr/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/uk/changelogs/100.txt b/fastlane/metadata/android/uk/changelogs/100.txt new file mode 100644 index 0000000..845c0a6 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Підтримка редагування дописів +- Нове налаштування для керування бажаним напрямком читання +- Більші прев'ю медіа та новий вигляд позначення медіа з описом +- З'явилася можливість додавати облікові записи до списків з їхнього профілю +та багато іншого diff --git a/fastlane/metadata/android/uk/changelogs/103.txt b/fastlane/metadata/android/uk/changelogs/103.txt new file mode 100644 index 0000000..0a5a5dc --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/103.txt @@ -0,0 +1,18 @@ +Tusky 22.0 beta 1 + +Включено функції: + +- Перегляд популярних хештегів +- Редагування описів зображень та точки фокусування +- Меню "Оновити" для доступності +- Підтримка фільтрів Mastodon v4 +- Показ докладних розбіжностей під час редагування допису +- Можливість показу статистики дописів у стрічці часу + +Виправлення: + +- Показ елементів керування програвачем під час відтворення аудіо +- Коректне обчислення довжини допису +- Завжди публікувати підписи до зображень + +та багато іншого diff --git a/fastlane/metadata/android/uk/changelogs/104.txt b/fastlane/metadata/android/uk/changelogs/104.txt new file mode 100644 index 0000000..aaa7040 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/104.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 2 + +Виправлення: + +- Покращено швидкість завантаження сповіщень +- Відновлено показ 0/1/1+ для відповідей +- Показ заголовків фільтрів, а не ключових слів у відфільтрованих повідомленнях +- Виправлено помилку, коли після відкриття статусу могло відкриватися незв'язане з ним посилання +- Кнопка "Додати" показується в правильному місці, коли немає фільтрів +- Усунено різноманітні збої diff --git a/fastlane/metadata/android/uk/changelogs/105.txt b/fastlane/metadata/android/uk/changelogs/105.txt new file mode 100644 index 0000000..7640fed --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/105.txt @@ -0,0 +1,11 @@ +Tusky 22.0 beta 3 + +Виправлення: + +- Усунуто збій під час перегляду гілки +- Виправлено обробку збоїв у фільтрах Mastodon +- Посилання в біо на сповіщення про запити на підписку/стеження можна натиснути +- Оновлення сповіщень Android + - Сповіщення Android для сповіщень Mastodon з'являється лише один раз + - Сповіщення Android групуються за типами сповіщень Mastodon (стеження, згадки, поширення тощо) + - Усунуто ймовірність зникнення сповіщень diff --git a/fastlane/metadata/android/uk/changelogs/106.txt b/fastlane/metadata/android/uk/changelogs/106.txt new file mode 100644 index 0000000..2b2161e --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/106.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 4 + +Виправлення: + +- Виправлено повторне отримання сповіщень, якщо налаштовано кілька облікових записів diff --git a/fastlane/metadata/android/uk/changelogs/107.txt b/fastlane/metadata/android/uk/changelogs/107.txt new file mode 100644 index 0000000..37fad82 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/107.txt @@ -0,0 +1,6 @@ +Tusky 22.0 beta 5 + +Виправлення: + +- Відкотили бібліотеку APNG для виправлення несправних анімованих емодзі +- Збереження локальної копії маркера сповіщень на випадок, якщо сервер не підтримує API diff --git a/fastlane/metadata/android/uk/changelogs/108.txt b/fastlane/metadata/android/uk/changelogs/108.txt new file mode 100644 index 0000000..a2c23e9 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/108.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 6 + +Виправлення: + +- Частіше збереження позиції читання на вкладці Сповіщення diff --git a/fastlane/metadata/android/uk/changelogs/109.txt b/fastlane/metadata/android/uk/changelogs/109.txt new file mode 100644 index 0000000..05e00e5 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/109.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 7 + +Виправлення: + + +### Значні виправлення помилок + +- Під час створення сповіщень Mastodon для Android виводитиме всі сповіщення Mastodon, що залишилися без відповіді +- Натискання кнопки "Створити" у сповіщенні могло призвести до встановлення неправильного облікового запису +- "ID останнього прочитаного сповіщення" відтепер зберігатиметься в правильному обліковому записі diff --git a/fastlane/metadata/android/uk/changelogs/110.txt b/fastlane/metadata/android/uk/changelogs/110.txt new file mode 100644 index 0000000..b89b7d0 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/110.txt @@ -0,0 +1,20 @@ +Tusky 22.0 + +Нові функції: + +- Перегляд популярних хештегів +- Стеження за новими хештегами +- Краще впорядкування вибору мов +- Показ різниці між версіями допису +- Підтримка фільтрів Mastodon v4 +- Можливість показу статистики дописів у стрічці +- Та інше... + +Виправлення: + +- Запам'ятовування вибраної вкладки та позиції +- Збереження сповіщень до прочитання +- Правильний показ змішаного RTL і LTR тексту в профілях +- Правильне обчислення довжини допису +- Підписи зображень публікуються завжди +- Та інше... diff --git a/fastlane/metadata/android/uk/changelogs/111.txt b/fastlane/metadata/android/uk/changelogs/111.txt new file mode 100644 index 0000000..da49c84 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/111.txt @@ -0,0 +1,15 @@ +Tusky 23.0 beta 1 + +Нові функції: + +- Новий параметр масштабу тексту + +Виправлення: + +- Правильне збереження інформації про обліковий запис +- Сповіщення "потягнути" на пристроях з Android <= 11 +- Усунено помилку в Android, коли з текстових полів не можна копіювати/вставляти +- Перегляд "розбіжностей" історії змін не виходитиме за межі екрана +- Якщо на вашому сервері немає історії зміни повідомлень не стається збоїв +- Додано кнопку "Видалити" за зміни фільтра +- Правильний показ неквадратних емодзі diff --git a/fastlane/metadata/android/uk/changelogs/112.txt b/fastlane/metadata/android/uk/changelogs/112.txt new file mode 100644 index 0000000..90f2af6 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/112.txt @@ -0,0 +1,6 @@ +Tusky 23.0 beta 2 + +Виправлено: + +- Потенційний збій під час редагування полів профілю +- Завелике контекстне меню під час редагування описів зображень diff --git a/fastlane/metadata/android/uk/changelogs/113.txt b/fastlane/metadata/android/uk/changelogs/113.txt new file mode 100644 index 0000000..f2722e4 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/113.txt @@ -0,0 +1,15 @@ +Tusky 23.0 + +Нове: + +- Новий параметр масштабу тексту + +Виправлення: + +- Правильне збереження інформації про обліковий запис +- Сповіщення "потягнути" на пристроях з Android <= 11 +- Усунено помилку в Android, коли з текстових полів не можна копіювати/вставляти +- Перегляд розбіжностей історії змін не виходитиме за межі екрана +- Якщо на вашому сервері немає історії зміни повідомлень не стається збоїв +- Правильний показ неквадратних емодзі +- Потенційний збій під час редагування полів профілю diff --git a/fastlane/metadata/android/uk/changelogs/115.txt b/fastlane/metadata/android/uk/changelogs/115.txt new file mode 100644 index 0000000..e5310ab --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/115.txt @@ -0,0 +1,10 @@ +Tusky 24.0 + +- Блок-цитати та блоки коду в дописах тепер приємніші на вигляд. +- Відновлено стару поведінку вкладки сповіщень. +- У профілях тепер показані бейджики ролей. +- Покращено роботу відеопрогравача. З'явилася можливість вибору швидкості відтворення. +- Нова опція теми для використання чорної теми при дотриманні дизайну системи. +- Новий вид для перегляду популярних дописів доступний як в меню, так і у вигляді кастомної вкладки. + +І багато інших покращень та виправлень! diff --git a/fastlane/metadata/android/uk/changelogs/117.txt b/fastlane/metadata/android/uk/changelogs/117.txt new file mode 100644 index 0000000..57adc1a --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/117.txt @@ -0,0 +1,7 @@ +Tusky 24.1 + +- Екран залишатиметься увімкненим під час відтворення відео. +- Виправлено витік пам'яті. Це повинно поліпшити стабільність і швидкодію. +- Емодзі тепер коректно рахуються як 1 символ під час написання повідомлення. +- Виправлено збій при виборі тексту на деяких пристроях. +- Піктограми в текстах довідки порожніх часових шкал тепер завжди будуть правильно вирівняні. diff --git a/fastlane/metadata/android/uk/changelogs/119.txt b/fastlane/metadata/android/uk/changelogs/119.txt new file mode 100644 index 0000000..55eeced --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/119.txt @@ -0,0 +1,8 @@ +Tusky 25 + +- Підтримка API перекладів Mastodon translation +- Показ мови домену +- Покращені переходи між екранами +- Налаштування фільтрів перенесено до налаштувань облікового запису +- Статистика дописів тепер має стабільну позицію +- Значне поліпшення стабільності та швидкодії під капотом diff --git a/fastlane/metadata/android/uk/changelogs/58.txt b/fastlane/metadata/android/uk/changelogs/58.txt new file mode 100644 index 0000000..bdfc651 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/58.txt @@ -0,0 +1,13 @@ +Tusky v6.0 + +- Фільтри стрічки в Налаштуваннях облікового запису та їхня синхронізація з сервером +- Можна мати власний хештег вкладкою основного інтерфейсу +- Списки можна редагувати +- Безпека: вилучено підтримку TLS 1.0 та TLS 1.1 та додано підтримку TLS 1.3 на Android 6+ +- Пропонування власних смайлів, під час введення +- «Слідувати системній темі» +- Поліпшено доступність стрічки +- Нехтування невідомими сповіщеннями без збоїв +- Можна змінювати мову +- Нові переклади: чеська та есперанто +- Інше diff --git a/fastlane/metadata/android/uk/changelogs/61.txt b/fastlane/metadata/android/uk/changelogs/61.txt new file mode 100644 index 0000000..3c184a0 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +- Підтримка показу опитувань, голосування та повідомлень про опитування +- Нові кнопки фільтрування вкладки сповіщень та видалення всіх сповіщень +- видалити та переробити власні дмухи +- новий індикатор, який показує, чи є обліковий запис ботом на зображенні профілю (можна вимкнути в налаштуваннях) +- Нові переклади: норвезька букмол та словенська. diff --git a/fastlane/metadata/android/uk/changelogs/67.txt b/fastlane/metadata/android/uk/changelogs/67.txt new file mode 100644 index 0000000..6c3eb97 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Тепер ви можете створювати опитування з Tusky +- Вдосконалено пошук +- Новий параметр налаштувань облікового запису — завжди розгортати попередження щодо вмісту +- Аватари в навігаційній панелі тепер мають округлу квадратну форму +- Тепер можна повідомляти про користувачів, навіть якщо вони ніколи не розміщували статус +- Tusky тепер відмовиться під'єднуватись через незахищене з’єднання на Android 6+ +- Багато інших невеликих удосконалень та виправлень diff --git a/fastlane/metadata/android/uk/changelogs/68.txt b/fastlane/metadata/android/uk/changelogs/68.txt new file mode 100644 index 0000000..e649dd6 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +Цей випуск забезпечує сумісність з Mastodon 3, підвищує продуктивність і стабільність. diff --git a/fastlane/metadata/android/uk/changelogs/70.txt b/fastlane/metadata/android/uk/changelogs/70.txt new file mode 100644 index 0000000..cdec5d1 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +- Тепер ви можете закріпити статус і перелічити свої закладки в Tusky. +- Тепер ви можете запланувати дмухи з Tusky. Зверніть увагу, що час, який ви вибираєте, має бути не раніше 5 хвилин у майбутньому. +- Тепер ви можете додати списки на головний екран. +- Тепер ви можете розмістити аудіовкладення з Tusky. + +І багато інших невеликих поліпшень та виправлень помилок! diff --git a/fastlane/metadata/android/uk/changelogs/72.txt b/fastlane/metadata/android/uk/changelogs/72.txt new file mode 100644 index 0000000..e8d9e82 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- Сповіщення про нові запити, коли ваш обліковий запис заблоковано +- Нові функції, які можна перемикати на екрані налаштувань: + - вимкнути перекидання між вкладками + - показ діалогового вікна підтвердження, перед дмухом + - показ попереднього перегляду посилання в стрічці +- Розмови тепер можна приглушити +- Вдосконалено голосування +- Виправлено помилки, більшість з яких пов'язані зі створенням дмухів +- Поліпшено переклади diff --git a/fastlane/metadata/android/uk/changelogs/74.txt b/fastlane/metadata/android/uk/changelogs/74.txt new file mode 100644 index 0000000..6b039de --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- Вдосконалено основний інтерфейс — тепер ви можете переміщувати вкладки вниз +- Заглушивши користувача, ви також можете вирішити, чи ігнорувати його сповіщення +- Тепер ви можете відстежувати скільки завгодно хештегів на окремій вкладці хештегів +- Поліпшено спосіб показу описів медіа, тому він працює навіть для наддовгих описів + +Журнал усіх змін: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/uk/changelogs/77.txt b/fastlane/metadata/android/uk/changelogs/77.txt new file mode 100644 index 0000000..fbbda2d --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- підтримка приміток профілю (функція Mastodon 3.2.0) +- підтримка повідомлень адміністратора (функція Mastodon 3.1.0) + +- аватар вибраного облікового запису тепер з'явиться на головній панелі інструментів +- натискання показуваного імені в стрічці відкриє сторінку профілю цього користувача + +- багато виправлень та невеликих удосконалень +- вдосконалено переклади diff --git a/fastlane/metadata/android/uk/changelogs/80.txt b/fastlane/metadata/android/uk/changelogs/80.txt new file mode 100644 index 0000000..e4cccad --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- Отримуйте сповіщення, коли користувач, який стежить за дописами, клацне піктограму дзвоника в його профілі! (Функція Mastodon 3.3.0) +- Функція чернетки в Tusky була повністю перероблена, щоб стати швидшою, зручнішою для користувачів і з меншою кількістю помилок. +- Додано новий режим добробуту, який дозволяє обмежити певні функції Tusky. +- Tusky тепер може анімувати власні смайли. +Повний журнал змін: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/uk/changelogs/82.txt b/fastlane/metadata/android/uk/changelogs/82.txt new file mode 100644 index 0000000..7e9a5be --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Запити на підписку тепер завжди показано в головному меню. +- Тепер вибір часу для планування публікації має дизайн, який відповідає решті програми +Журнал усіх змін: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/uk/changelogs/83.txt b/fastlane/metadata/android/uk/changelogs/83.txt new file mode 100644 index 0000000..d8e3f5f --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +У цьому випуску виправлено збої під час захоплення зображень diff --git a/fastlane/metadata/android/uk/changelogs/87.txt b/fastlane/metadata/android/uk/changelogs/87.txt new file mode 100644 index 0000000..3a5e4a7 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- Повністю переписано логіку завантаження подій для пришвидшення роботи, зменшення вад та полегшення підтримки. +- Tusky тепер може анімувати користувацькі емоджі у форматі APNG та Animated WebP. +- Виправлено багато помилок +- Підтримка для Android 11 +- Нові переклади: шотландською гельською, галісійською, українською мовами +- Удосконалено переклади diff --git a/fastlane/metadata/android/uk/changelogs/89.txt b/fastlane/metadata/android/uk/changelogs/89.txt new file mode 100644 index 0000000..fbed5e8 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- «Відкрити як...» тепер також доступно в меню профілів облікових записів за користування кількома обліковими записами +- Тепер вхід обробляється у WebView у застосунку +- Підтримка Android 12 +- підтримка нового API конфігурації сервера Mastodon +- і багато інших дрібних виправлень і вдосконалень diff --git a/fastlane/metadata/android/uk/changelogs/91.txt b/fastlane/metadata/android/uk/changelogs/91.txt new file mode 100644 index 0000000..4132d15 --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Підтримка нових типів сповіщень Mastodon 3.5 +- Кращий вигляд позначки бота і розширений вибір тем +- Текст тепер можна вибрати у докладному поданні допису +- Виправлено безліч помилок, включно з тою, яка перешкоджала входу на Android 6 і старіших diff --git a/fastlane/metadata/android/uk/changelogs/94.txt b/fastlane/metadata/android/uk/changelogs/94.txt new file mode 100644 index 0000000..a31309f --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Підтримка Unified Push. Щоб активувати підтримку, вам потрібно повторно увійти в обліковий запис. +- Кількість відповідей на допис тепер вказана у стрічках. +- Зображення тепер можуть обрізатися під час складання допису. +- Профілі тепер показують дату їхнього створення. +- Під час перегляду списку назва відтепер показана на панелі інструментів. +- Усунення помилок +- Покращення перекладу diff --git a/fastlane/metadata/android/uk/changelogs/97.txt b/fastlane/metadata/android/uk/changelogs/97.txt new file mode 100644 index 0000000..233daff --- /dev/null +++ b/fastlane/metadata/android/uk/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- Нова піктограма застосунку від Dzuk https://dzuk.zone/ +- Додано можливість слідкувати за хештегами. Натисніть на нього, а потім на піктограму на панелі інструментів. +- Підтримка Android 13 +- новий спадний список у вікні створення допису для вибору мови допису +- Вкладка «Медіа» у профілях завантажується плавніше для чутливих носіїв. +- З'явилася можливість установити фокусувати зображення перед оприлюдненням +- Нова опція показу вашого повного імені користувача на панелі інструментів diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt new file mode 100644 index 0000000..2a76e23 --- /dev/null +++ b/fastlane/metadata/android/uk/full_description.txt @@ -0,0 +1,12 @@ +Tusky — це легкий клієнт для Mastodon, безплатної соціальної мережі з відкритим кодом сервера. + +• Дизайн в стилі Material +• Підтримка більшості можливостей Mastodon +• Підтримка кількох облікових записів +• Темна та світла теми з можливістю автоперемикання залежно від часу доби +• Чернетки — почніть створювати допис і збережіть його на потім +• Вибір між різними наборами емодзі +• Застосунок оптимізовано для різних розмірів екрана +• Відкритий джерельний код, ніяких не вільних складників, як-от служб Google. + +Щоб дізнатися більше про Mastodon, відвідайте https://joinmastodon.org/ diff --git a/fastlane/metadata/android/uk/short_description.txt b/fastlane/metadata/android/uk/short_description.txt new file mode 100644 index 0000000..33eeef0 --- /dev/null +++ b/fastlane/metadata/android/uk/short_description.txt @@ -0,0 +1 @@ +Клієнт соціальної мережі Mastodon з підтримкою кількох облікових записів diff --git a/fastlane/metadata/android/uk/title.txt b/fastlane/metadata/android/uk/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/uk/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/vi/changelogs/100.txt b/fastlane/metadata/android/vi/changelogs/100.txt new file mode 100644 index 0000000..2a09932 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- Hỗ trợ sửa tút +- Thêm cài đặt cách đọc +- Xem trước media và lớp phủ mới để biểu thị phương tiện có mô tả +- Cho phép thêm tài khoản vào danh sách hồ sơ +và còn nữa diff --git a/fastlane/metadata/android/vi/changelogs/103.txt b/fastlane/metadata/android/vi/changelogs/103.txt new file mode 100644 index 0000000..2ff999f --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/103.txt @@ -0,0 +1,18 @@ +Tusky 22.0 + +Tính năng: + +- Hiện hashtag nổi bật +- Sửa mô tả ảnh và trọng tâm ảnh +- Menu "Làm tươi" +- Hỗ trợ bộ lọc Mastodon v4 +- Hiện thông tin khi một tút có sửa đổi +- Tùy chọn hiện số tương tác trên bảng tin + +Sửa lỗi: + +- Hiện nút điều khiển khi phát âm thanh +- Lỗi độ dài tút +- Luôn đăng mô tả ảnh + +và nhiều nữa diff --git a/fastlane/metadata/android/vi/changelogs/104.txt b/fastlane/metadata/android/vi/changelogs/104.txt new file mode 100644 index 0000000..dffabc4 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/104.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 2 + +Fixes including: + +- Improved notification loading speed +- Restore showing 0/1/1+ for replies +- Show filter titles, not filter keywords, on filtered posts +- Fixed a bug where opening a status could open an unrelated link +- Show "Add" button in correct place when there are no filters +- Fixed assorted crashes diff --git a/fastlane/metadata/android/vi/changelogs/105.txt b/fastlane/metadata/android/vi/changelogs/105.txt new file mode 100644 index 0000000..0dc1147 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/105.txt @@ -0,0 +1,11 @@ +Tusky 22.0 beta 3 + +Fixes including: + +- Fixed crash when viewing a thread +- Fixed crash processing Mastodon filters +- Links in bios of follow/follow request notifications are clickable +- Android Notifications updates + - Android notification for a Mastodon notification should only be shown once + - Android notifications are grouped by Mastodon notification type (follow, mention, boost, etc) + - Potential for missing notifications has been removed diff --git a/fastlane/metadata/android/vi/changelogs/106.txt b/fastlane/metadata/android/vi/changelogs/106.txt new file mode 100644 index 0000000..28785ce --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/106.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 4 + +Fixes: + +- Fixed repeated fetch of notifications if configured with multiple accounts diff --git a/fastlane/metadata/android/vi/changelogs/107.txt b/fastlane/metadata/android/vi/changelogs/107.txt new file mode 100644 index 0000000..7298707 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/107.txt @@ -0,0 +1,6 @@ +Tusky 22.0 beta 5 + +Fixes: + +- Rolled back APNG library to fix broken animated emojis +- Save local copy of notification marker in case server does not support the API diff --git a/fastlane/metadata/android/vi/changelogs/108.txt b/fastlane/metadata/android/vi/changelogs/108.txt new file mode 100644 index 0000000..f4e1b0c --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/108.txt @@ -0,0 +1,5 @@ +Tusky 22.0 beta 6 + +Fixes: + +- Save reading position in the Notifications tab more frequently diff --git a/fastlane/metadata/android/vi/changelogs/109.txt b/fastlane/metadata/android/vi/changelogs/109.txt new file mode 100644 index 0000000..edc99c8 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/109.txt @@ -0,0 +1,10 @@ +Tusky 22.0 beta 7 + +Fixes: + + +### Significant bug fixes + +- Fetch all outstanding Mastodon notifications when creating Android notifications +- Clicking "Compose" from a notification would set the wrong account +- Ensure "last read notification ID" is saved to the correct account diff --git a/fastlane/metadata/android/vi/changelogs/110.txt b/fastlane/metadata/android/vi/changelogs/110.txt new file mode 100644 index 0000000..330f7a3 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/110.txt @@ -0,0 +1,18 @@ +Tusky 22.0 + +Tính năng mới: +- Xem hashtag xu hướng +- Theo dõi hashtag +- Chọn ngôn ngữ dễ hơn +- Xem lịch sử sửa tút +- Hỗ trợ bộ lọc Mastodon v4 +- Hiện số tương tác tút trên bảng tin +- Còn nữa... + +Sửa: +- Nhớ tab đã chọn và vị trí +- Giữ thông báo cho tới khi đọc +- Nội dung RTL và LTR trên hồ sơ +- Tính toán độ dài tút +- Luôn đăng mô tả ảnh +- Còn nữa... diff --git a/fastlane/metadata/android/vi/changelogs/111.txt b/fastlane/metadata/android/vi/changelogs/111.txt new file mode 100644 index 0000000..2b3e55f --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/111.txt @@ -0,0 +1,15 @@ +Tusky 23.0 beta 1 + +Mới: + +- Tăng cỡ chữ + +Sửa: + +- Lưu đúng thông tin tài khoản +- "đẩy" thông báo trên Android 11 trở xuống +- Sửa lỗi Android quên copy/paste +- Hiện "diffs" trong lịch sử sửa tút +- Sửa lỗi crash +- Thêm nút "Xóa" khi sửa bộ lọc +- Hiện đúng emoji không vuông diff --git a/fastlane/metadata/android/vi/changelogs/112.txt b/fastlane/metadata/android/vi/changelogs/112.txt new file mode 100644 index 0000000..c7d6d38 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/112.txt @@ -0,0 +1,6 @@ +Tusky 23.0 beta 2 + +Sửa: + +- Sửa crash khi sửa hồ sơ +- Mở rộng menu khi sửa mô tả hình ảnh diff --git a/fastlane/metadata/android/vi/changelogs/113.txt b/fastlane/metadata/android/vi/changelogs/113.txt new file mode 100644 index 0000000..b31425a --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/113.txt @@ -0,0 +1,13 @@ +Tusky 23.0 + +Mới: +- Thu phóng chữ trên giao diện + +Sửa: +- Lưu thông tin tài khoản chính xác +- "đẩy" thông báo trên Android 11 +- Lỗi copy/paste không được +- Xem lịch sử sửa tút +- Không crash khi máy chủ của bạn không có lịch sử sửa +- Hiện emoji không vuông chính xác +- Lỗi crash khi sửa hồ sơ diff --git a/fastlane/metadata/android/vi/changelogs/115.txt b/fastlane/metadata/android/vi/changelogs/115.txt new file mode 100644 index 0000000..5eb9f13 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/115.txt @@ -0,0 +1,10 @@ +Tusky 24.0 + +- Blockquotes đẹp hơn. +- Tab thông báo trở về như cũ. +- Hiện huy hiệu vai trò trên hồ sơ. +- Cải thiện trình phát video. Cho chọn tốc độ phát. +- Thêm chủ đề đen. +- Hiện những tút thịnh hành. + +Và các cải thiện khác! diff --git a/fastlane/metadata/android/vi/changelogs/117.txt b/fastlane/metadata/android/vi/changelogs/117.txt new file mode 100644 index 0000000..b2cbb00 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/117.txt @@ -0,0 +1,7 @@ +Tusky 24.1 + +- Màn hình đứng im khi phát video. +- Sửa lỗi memory leak. +- Emoji được tính như 1 ký tự. +- Sửa lỗi crash khi chọn text. +- Canh đều biểu tượng trợ giúp. diff --git a/fastlane/metadata/android/vi/changelogs/119.txt b/fastlane/metadata/android/vi/changelogs/119.txt new file mode 100644 index 0000000..a790aeb --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/119.txt @@ -0,0 +1,8 @@ +Tusky 25 + +- Hỗ trợ Mastodon Dịch API +- Hiện ngôn ngữ tút +- Cải thiện chuyển đổi màn hình +- Cài đặt bộ lọc được chuyển sang tùy chọn tài khoản +- Thống kê bài viết bây giờ đã có vị trí ổn định +- Rất nhiều cải tiến về độ ổn định và hiệu suất bên trong diff --git a/fastlane/metadata/android/vi/changelogs/58.txt b/fastlane/metadata/android/vi/changelogs/58.txt new file mode 100644 index 0000000..687ac1e --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/58.txt @@ -0,0 +1,10 @@ +Tusky 6.0 + +- Tùy chỉnh hashtag +- Chỉnh sửa danh sách +- Bảo mật +- Gợi ý emoji +- Cài đặt chủ đề mới +- Bỏ qua các thông chưa biết +- Thiết lập ngôn ngữ khác +- Bản dịch mới: Czech và Esperanto diff --git a/fastlane/metadata/android/vi/changelogs/61.txt b/fastlane/metadata/android/vi/changelogs/61.txt new file mode 100644 index 0000000..501ec53 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky 7.0 + +- Hỗ trợ hiển thị bình chọn +- Thêm nút mới trên tab +- xóa và viết lại tút +- Hiển thị danh tính Bot +- Bản dịch mới: Norwegian Bokmål và Slovenian. diff --git a/fastlane/metadata/android/vi/changelogs/67.txt b/fastlane/metadata/android/vi/changelogs/67.txt new file mode 100644 index 0000000..a73b6d1 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +- Tạo bình chọn +- Cải thiện tìm kiếm +- Cài đặt mở rộng nội dung nhạy cảm +- Thêm bo viền cho ảnh đại diện +- Thêm tính năng báo cáo +- Fix cleartext connections trên Android 6+ +- Cải thiện và sửa lỗi diff --git a/fastlane/metadata/android/vi/changelogs/68.txt b/fastlane/metadata/android/vi/changelogs/68.txt new file mode 100644 index 0000000..57bdbff --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky 9.1 + +Cải tiến sự tương thích với Mastodon 3 cũng như giao diện đồ họa. diff --git a/fastlane/metadata/android/vi/changelogs/70.txt b/fastlane/metadata/android/vi/changelogs/70.txt new file mode 100644 index 0000000..8f4f34c --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky 10.0 + +- Bạn có thể lưu tút với Tusky. +- Bạn có thể lên lịch đăng tút với Tusky. +- Bạn có thể thêm danh sách. +- Bạn có thể đăng tập tin âm thanh với Tusky. + +Nhiều cải thiện nhỏ và sửa lỗi khác! diff --git a/fastlane/metadata/android/vi/changelogs/72.txt b/fastlane/metadata/android/vi/changelogs/72.txt new file mode 100644 index 0000000..7dde733 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/72.txt @@ -0,0 +1,8 @@ +Tusky 11.0 + +- Thông báo yêu cầu theo dõi mới +- Thêm tính năng trong màn hình Cài Đặt +- Có thể ẩn cuộc đối thoại +- Cải thiện thuật toán Bình Chọn +- Sửa lỗi liên quan khi viết tút +- Cải thiện bản dịch diff --git a/fastlane/metadata/android/vi/changelogs/74.txt b/fastlane/metadata/android/vi/changelogs/74.txt new file mode 100644 index 0000000..45e0393 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/74.txt @@ -0,0 +1,9 @@ +Tusky 12.0 + +- Cải thiện giao diện - giờ bạn có thể chuyển tab lên trên +- Khi ẩn ai đó, bạn có thể chọn ẩn thông báo từ họ hay không +- Tab Hashtag giờ đã hiện tất cả hashtag +- Cải thiện phần mô tả đa phương tiện +- Thêm ngôn ngữ: Irish, Hindi, Vietnamese và Thai + +Full changelog: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/vi/changelogs/77.txt b/fastlane/metadata/android/vi/changelogs/77.txt new file mode 100644 index 0000000..11a1d6f --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/77.txt @@ -0,0 +1,8 @@ +Tusky v13.0 + +- Hỗ trợ ghi chú về một ai đó (tính năng Mastodon 3.2.0) +- Hỗ trợ hiện thông báo máy chủ (tính năng Mastodon 3.1.0) +- Ảnh đại diện của tài khoản từ giờ sẽ hiện trên thanh menu chính +- Nhấn vào tên ai đó trên bảng tin sẽ chuyển tới trang cá nhân của họ +- Sửa lỗi linh tinh và cải thiện hiệu năng +- Trau dồi bản dịch diff --git a/fastlane/metadata/android/vi/changelogs/80.txt b/fastlane/metadata/android/vi/changelogs/80.txt new file mode 100644 index 0000000..3d54a1a --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/80.txt @@ -0,0 +1,9 @@ +Tusky v14.0 + +- Thông báo khi người bạn theo dõi đăng tút - click vào biểu tượng cái chuông trên trang cá nhân của họ! (Mastodon 3.3.0) +- Tút Nháp: được thiết kế lại toàn bộ, giúp nhanh hơn, dễ dùng hơn và ít lỗi hơn. +- Chế độ Cai Nghiện: cho phép bạn giới hạn một số tính năng của Tusky. +- Hỗ trợ Emoji động: cho phép xem emoji động trong Tusky. +- Ẩn Có Thời Hạn: có thể chặn người nào đó trong khoảng thời gian cho trước. +- Sửa các lỗi vặt, đặc biệt là sự tương thích Pleroma. +- Cải thiện bản dịch diff --git a/fastlane/metadata/android/vi/changelogs/82.txt b/fastlane/metadata/android/vi/changelogs/82.txt new file mode 100644 index 0000000..3aa0b88 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- Hiện yêu cầu theo dõi trên menu chính. +- Cải thiện thời gian lên lịch tút với thiết kế thuận tiện hơn. +Full changelog: https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/vi/changelogs/83.txt b/fastlane/metadata/android/vi/changelogs/83.txt new file mode 100644 index 0000000..68839be --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +Sửa lỗi crash khi ghi chú cho hình diff --git a/fastlane/metadata/android/vi/changelogs/87.txt b/fastlane/metadata/android/vi/changelogs/87.txt new file mode 100644 index 0000000..a6fe6a0 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- Viết lại hoàn toàn phương thức tải bảng tin để tải nhanh hơn, ít lỗi hơn và dễ bảo trì +- Hỗ trợ emoji định dạng APNG & WebP động +- Sửa nhiều lỗi khác +- Hỗ trợ Android 11 +- Ngôn ngữ mới: Scottish Gaelic, Galician, Ukrainian +- Cải thiện bản dịch diff --git a/fastlane/metadata/android/vi/changelogs/89.txt b/fastlane/metadata/android/vi/changelogs/89.txt new file mode 100644 index 0000000..c77487c --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- Hiện "Mở bằng..." trong menu Tài khoản kể cả khi dùng nhiều tài khoản +- Hiện đăng nhập bằng WebView ngay trong app +- Hỗ trợ Android 12 +- Hỗ trợ thiết lập API máy chủ Mastodon mới +- Sửa lỗi nhỏ và cải thiện diff --git a/fastlane/metadata/android/vi/changelogs/91.txt b/fastlane/metadata/android/vi/changelogs/91.txt new file mode 100644 index 0000000..2835fdf --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- Hỗ trợ những kiểu thông báo mới của Mastodon 3.5 +- Nhãn của tài khoản nhìn đẹp hơn và thay đổi theo chủ đề +- Cho phép chọn và sao chép nội dung tút +- Sửa lỗi chặn đăng nhập trên Android 6 trở xuống diff --git a/fastlane/metadata/android/vi/changelogs/94.txt b/fastlane/metadata/android/vi/changelogs/94.txt new file mode 100644 index 0000000..b3f8aa2 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- Hỗ trợ Unified Push. Bạn cần đăng nhập lại để sử dụng được. +- Hiện số lượng trả lời trên nút +- Cắt ảnh khi viết tút +- Hiện ngày tham gia Mastodon +- Khi xem danh sách, tựa đề sẽ hiện trên toolbar +- Sửa lỗi vặt +- Cải thiện bản dịch diff --git a/fastlane/metadata/android/vi/changelogs/97.txt b/fastlane/metadata/android/vi/changelogs/97.txt new file mode 100644 index 0000000..492c721 --- /dev/null +++ b/fastlane/metadata/android/vi/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- Biểu tượng app mới của Dzuk https://dzuk.zone/ +- Theo dõi hashtag. Nhấn vào một hashtag và sau đó nhấn vào biểu tượng trên thanh công cụ. +- Hỗ trợ Android 13 +- Chọn ngôn ngữ của tút +- Tab media trong hồ sơ hiện tôn trọng media nhạy cảm và tải mượt mà hơn. +- Đặt điểm lấy nét của ảnh trước khi đăng +- Tùy chọn mới để hiển thị tên người dùng đầy đủ của bạn trên thanh công cụ diff --git a/fastlane/metadata/android/vi/full_description.txt b/fastlane/metadata/android/vi/full_description.txt new file mode 100644 index 0000000..9bdce42 --- /dev/null +++ b/fastlane/metadata/android/vi/full_description.txt @@ -0,0 +1,12 @@ +Tusky là ứng dụng di động nhỏ gọn dành cho Mastodon, một mạng xã hội tự do và mã nguồn mở. + +• Thiết kế tinh tế +• Thực thi hầu hết Mastodon API +• Hỗ trợ đăng nhập nhiều tài khoản +• Giao diện sáng hoặc tối, tự động kích hoạt dựa theo thời gian của thiết bị +• Viết và lưu nháp tút +• Tùy chọn nhiều bộ Emoji khác nhau +• Tối ưu hóa cho mọi kích cỡ màn hình +• Mã nguồn mở hoàn toàn + +Để tìm hiểu thêm Mastodon, truy cập https://joinmastodon.org/ diff --git a/fastlane/metadata/android/vi/short_description.txt b/fastlane/metadata/android/vi/short_description.txt new file mode 100644 index 0000000..6ed0e74 --- /dev/null +++ b/fastlane/metadata/android/vi/short_description.txt @@ -0,0 +1 @@ +Một ứng dụng di động cho mạng xã hội Mastodon diff --git a/fastlane/metadata/android/vi/title.txt b/fastlane/metadata/android/vi/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/vi/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/zh-Hans/changelogs/100.txt b/fastlane/metadata/android/zh-Hans/changelogs/100.txt new file mode 100644 index 0000000..bdd784c --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/100.txt @@ -0,0 +1,7 @@ +Tusky 21.0 + +- 支持编辑嘟文 +- 控制偏首选阅读方向的新设置 +- 支持预览更大的媒体文件以及表明带描述媒体文件的新遮罩 +- 允许从账户的资料页将其添加到列表 +还有其他更多内容有待发现 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/58.txt b/fastlane/metadata/android/zh-Hans/changelogs/58.txt new file mode 100644 index 0000000..515972c --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/58.txt @@ -0,0 +1,13 @@ +Tusky v6.0 + +-时间轴过滤器已移至“账户首选项”,并将与服务器同步 +-现在,您可以在主界面中将自定义主题标签作为标签 +-现在可以编辑列表 +-安全性:删除了对 TLS 1.0 和 TLS 1.1 的支持,并在 Android 6+ 上添加了对 TLS 1.3 的支持 +-现在,撰写视图将在开始键入时建议自定义表情符号 +-新的主题设置“遵循系统主题” +-改善了时间表的可访问性 +-Tusky 现在将忽略未知的通知,不再崩溃 +-新设置:您现在可以覆盖系统语言,并在 Tusky 中设置其他语言 +-新译文:捷克文和世界语 +-其他许多改进和修正 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/61.txt b/fastlane/metadata/android/zh-Hans/changelogs/61.txt new file mode 100644 index 0000000..7f8e520 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/61.txt @@ -0,0 +1,7 @@ +Tusky v7.0 + +-支持显示投票,投票和投票通知 +-新按钮可过滤通知标签并删除所有通知 +-删除并重新草拟自己的嘟文 +-新的指示器,用于在个人资料图片上显示账户是否为漫游器(可以在首选项中关闭) +-新的翻译:挪威博克马尔语和斯洛文尼亚语。 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/67.txt b/fastlane/metadata/android/zh-Hans/changelogs/67.txt new file mode 100644 index 0000000..9bbbdf6 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/67.txt @@ -0,0 +1,9 @@ +Tusky v9.0 + +-您现在可以从 Tusky 创建投票 +-改进了搜索 +-账户首选项中的新选项可始终扩展内容警告 +-导航抽屉中的头像现在具有圆角正方形 +-现在即使用户从未发布过状态,也可以报告他们 +-Tusky 现在将拒绝通过 Android 6+ 上的明文连接进行连接 +-其他许多小改进和错误修复 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/68.txt b/fastlane/metadata/android/zh-Hans/changelogs/68.txt new file mode 100644 index 0000000..ab41264 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/68.txt @@ -0,0 +1,3 @@ +Tusky v9.1 + +此版本确保了与 Mastodon 3 的兼容性并提高了性能和稳定性。 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/70.txt b/fastlane/metadata/android/zh-Hans/changelogs/70.txt new file mode 100644 index 0000000..f394f1b --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/70.txt @@ -0,0 +1,8 @@ +Tusky v10.0 + +-现在,您可以为状态添加书签并在 Tusky 中列出您的书签。 +-您现在可以使用 Tusky 来安排嘟文。请注意,您选择的时间必须至少为5分钟。 +-您现在可以将列表添加到主屏幕。 +-您现在可以使用 Tusky 发布音频附件。 + +还有许多其他小的改进和错误修复! diff --git a/fastlane/metadata/android/zh-Hans/changelogs/72.txt b/fastlane/metadata/android/zh-Hans/changelogs/72.txt new file mode 100644 index 0000000..b947a18 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/72.txt @@ -0,0 +1,11 @@ +Tusky v11.0 + +- 锁嘟开启时通知新关注请求 +- 新设置: + - 禁用标签页间滑动 + - 转嘟前提示确认 + - 在时间轴上显示链接预览 +- 对话可被隐藏 +- 投票结果现在用投票人数计算而非用票数计算,从而使得多选投票结果更容易理解 +- 许多Bug修复,其中主要是一些和发嘟文相关的 Bug +- 改进翻译 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/74.txt b/fastlane/metadata/android/zh-Hans/changelogs/74.txt new file mode 100644 index 0000000..af72c4f --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/74.txt @@ -0,0 +1,8 @@ +Tusky v12.0 + +- 主界面改善,现在标签页可被放至底部 +- 隐藏用户时提供同时隐藏来自该用户的通知的选项 +- 同一标签页下可显示的话题数量现在不受限制 +- 改善媒体描述信息的显示方式,使其支持超长描述 + +完整更新日志:https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/zh-Hans/changelogs/77.txt b/fastlane/metadata/android/zh-Hans/changelogs/77.txt new file mode 100644 index 0000000..072a9dc --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/77.txt @@ -0,0 +1,10 @@ +Tusky v13.0 + +- 支持账号备注(Mastodon 3.2.0 特性) +- 支持公告栏(Mastodon 3.1.0特性) + +- 当前账号的头像将在导航栏显示 +- 在时间线中点击账号名称后打开该用户的资料页 + +- 其他许多小改进和错误修复 +- 改善翻译 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/80.txt b/fastlane/metadata/android/zh-Hans/changelogs/80.txt new file mode 100644 index 0000000..9648b20 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/80.txt @@ -0,0 +1,7 @@ +Tusky v14.0 + +- 点击账号详情页的铃铛即可在该用户发送了新嘟文时得到通知!(Mastodon 3.3.0 新功能) +- Tusky 的草稿功能已被重新设计,它现在更快、更友好,Bug也更少了。 +- 添加了健康模式,可限制特定的一些 Tusky 功能。 +- Tusky 现在可显示动态自定义Emoji。 +完整更新日志:https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/zh-Hans/changelogs/82.txt b/fastlane/metadata/android/zh-Hans/changelogs/82.txt new file mode 100644 index 0000000..3decf1f --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/82.txt @@ -0,0 +1,5 @@ +Tusky v15.0 + +- 关注请求将总在首页显示。 +- 预订嘟文时间选取器的设计风格现与 App 一致。 +完整更新日志:https://github.com/tuskyapp/Tusky/releases diff --git a/fastlane/metadata/android/zh-Hans/changelogs/83.txt b/fastlane/metadata/android/zh-Hans/changelogs/83.txt new file mode 100644 index 0000000..e8f7c36 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/83.txt @@ -0,0 +1,3 @@ +Tusky v15.1 + +此版本修复了给图片添加标题时会崩溃的问题 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/87.txt b/fastlane/metadata/android/zh-Hans/changelogs/87.txt new file mode 100644 index 0000000..8f75491 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/87.txt @@ -0,0 +1,8 @@ +Tusky v16.0 + +- 时间线加载逻辑完全重写,提升了流畅度、稳定性,更便于维护。 +- APNG 和动画 WebP 格式的动态自定义表情符号。 +- 修正大量 BUG +- 支持 Android 11 +- 新增界面语言支持:苏格兰盖尔语、加利西亚语、乌克兰语 +- 改进翻译 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/89.txt b/fastlane/metadata/android/zh-Hans/changelogs/89.txt new file mode 100644 index 0000000..6679be0 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/89.txt @@ -0,0 +1,7 @@ +Tusky v17.0 + +- 使用多个账户时,“打开为...”现在也可以在账户配置文件的菜单中使用。 +- 登录现在在内嵌的 WebView 中处理。 +- 支持 Android 12。 +- 支持新的 Mastodon 实例配置 API。 +- 一些其它小修复和改动 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/91.txt b/fastlane/metadata/android/zh-Hans/changelogs/91.txt new file mode 100644 index 0000000..e91fe6d --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/91.txt @@ -0,0 +1,6 @@ +Tusky v18.0 + +- 支持新的 Mastodon 3.5 通知类型。 +- 机器人徽章现在看起来更棒,并将适应所选主题。 +- 嘟文详情视图上的文本现在可以被选择。 +- 修复了一些 bugs,其中有一个会阻止在 Android 6 及更低版本的设备上登录 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/94.txt b/fastlane/metadata/android/zh-Hans/changelogs/94.txt new file mode 100644 index 0000000..31b090b --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/94.txt @@ -0,0 +1,9 @@ +Tusky 19.0 + +- 支持统一推送。 要激活支持,您必须重新登录您的账户。 +- 嘟文的回复数量现在显示在时间轴中。 +- 现在可以在撰写嘟文时裁剪图片。 +- 配置文件现在将显示创建日期。 +- 查看列表时,标题将显示在工具栏中。 +- 很多错误修正。 +- 翻译改进 diff --git a/fastlane/metadata/android/zh-Hans/changelogs/97.txt b/fastlane/metadata/android/zh-Hans/changelogs/97.txt new file mode 100644 index 0000000..4879192 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/changelogs/97.txt @@ -0,0 +1,9 @@ +Tusky 20.0 + +- 来自 Dzuk 的新图标 https://dzuk.zone/ +- 您现在可以关注主题标签。 点击主题标签后再点击工具栏中的图标即可。 +- 支持 Android 13 +- 撰写视图中用于设置嘟文语言的新下拉菜单 +- 配置文件中的媒体选项卡现在尊重敏感媒体且加载得更流畅。 +- 现在可以在发布前设置图片的焦点 +- 在工具栏中显示完整用户名的新选项 diff --git a/fastlane/metadata/android/zh-Hans/full_description.txt b/fastlane/metadata/android/zh-Hans/full_description.txt new file mode 100644 index 0000000..2cd341b --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/full_description.txt @@ -0,0 +1,12 @@ +Tusky 是 Mastodon(一个免费的开源社交网络服务器)的轻量级客户端。 + +• MD 设计 +• 支持大多数 Mastodon API +• 多账户支持 +• 深色和浅色主题,可以根据时间自动切换 +• 草稿 - 编写嘟文并将其保存以备后用 +• 选择不同的表情样式 +• 针对所有屏幕尺寸进行了优化 +• 完全开源 - 没有像 Google 服务这样的非自由依赖项 + +要了解有关Mastodon的更多信息,请访问 https://joinmastodon.org/ diff --git a/fastlane/metadata/android/zh-Hans/short_description.txt b/fastlane/metadata/android/zh-Hans/short_description.txt new file mode 100644 index 0000000..cad1486 --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/short_description.txt @@ -0,0 +1 @@ +社交网络 Mastodon 的多账户客户端 diff --git a/fastlane/metadata/android/zh-Hans/title.txt b/fastlane/metadata/android/zh-Hans/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/zh-Hans/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/fastlane/metadata/android/zh-Hant/full_description.txt b/fastlane/metadata/android/zh-Hant/full_description.txt new file mode 100644 index 0000000..9c79dd3 --- /dev/null +++ b/fastlane/metadata/android/zh-Hant/full_description.txt @@ -0,0 +1,12 @@ +Tusky 是 Mastodon(一個免費的開源社交網絡服務器)的輕量級客戶端。 + +• MD 設計 +• 支援大多數 Mastodon API +• 多帳戶支援 +• 深色和淺色主題,可以根據時間自動切換 +• 草稿 - 編寫嘟文並將其保存以備後用 +• 選擇不同的表情樣式 +• 針對所有屏幕尺寸進行了優化 +• 完全開源 - 沒有像 Google 服務這樣的非自由依賴項 + +要了解有關 Mastodon 的更多信息,請訪問 https://joinmastodon.org/ diff --git a/fastlane/metadata/android/zh-Hant/short_description.txt b/fastlane/metadata/android/zh-Hant/short_description.txt new file mode 100644 index 0000000..d846bc0 --- /dev/null +++ b/fastlane/metadata/android/zh-Hant/short_description.txt @@ -0,0 +1 @@ +社交網絡 Mastodon 的多帳戶客戶端 diff --git a/fastlane/metadata/android/zh-Hant/title.txt b/fastlane/metadata/android/zh-Hant/title.txt new file mode 100644 index 0000000..0238ffc --- /dev/null +++ b/fastlane/metadata/android/zh-Hant/title.txt @@ -0,0 +1 @@ +Tusky diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..655b430 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,11 @@ +org.gradle.caching=true +# If jvmargs is changed then the default values must also be included, see https://github.com/gradle/gradle/issues/19750 +org.gradle.jvmargs=-XX:+UseParallelGC -Xmx4g -Dfile.encoding=UTF-8 -XX:MaxMetaspaceSize=2g -XX:+HeapDumpOnOutOfMemoryError -Xms256m +# use parallel execution +org.gradle.parallel=true +org.gradle.configuration-cache=true + +# Disable buildFeatures flags by default +android.defaults.buildfeatures.resvalues=false +android.defaults.buildfeatures.shaders=false +android.useAndroidX=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..72db726 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,154 @@ +[versions] +agp = "8.5.0" +androidx-activity = "1.9.0" +androidx-appcompat = "1.7.0" +androidx-browser = "1.8.0" +androidx-cardview = "1.0.0" +androidx-constraintlayout = "2.1.4" +androidx-core = "1.13.1" +androidx-drawerlayout = "1.2.0" +androidx-exifinterface = "1.3.7" +androidx-fragment = "1.8.0" +androidx-hilt = "1.2.0" +androidx-junit = "1.1.5" +androidx-lifecycle = "2.8.2" +androidx-media3 = "1.3.1" +androidx-paging = "3.3.0" +androidx-preference = "1.2.1" +androidx-recyclerview = "1.3.2" +androidx-sharetarget = "1.2.0" +androidx-splashscreen = "1.2.0-alpha01" +androidx-swiperefresh-layout = "1.1.0" +androidx-testing = "2.2.0" +androidx-viewpager2 = "1.1.0" +androidx-work = "2.9.0" +androidx-room = "2.6.1" +bouncycastle = "1.70" +conscrypt = "2.5.2" +coroutines = "1.8.1" +diffx = "1.1.1" +emoji2 = "1.4.0" +espresso = "3.5.1" +filemoji-compat = "3.2.7" +glide = "4.16.0" +# Deliberate downgrade, https://github.com/tuskyapp/Tusky/issues/3631 +glide-animation-plugin = "2.23.0" +hilt = "2.51.1" +kotlin = "2.0.0" +image-cropper = "4.3.2" +material = "1.12.0" +material-drawer = "8.4.5" +material-typeface = "4.0.0.3-kotlin" +mockito-inline = "5.2.0" +mockito-kotlin = "5.3.1" +moshi = "1.15.1" +networkresult-calladapter = "1.1.0" +okhttp = "4.12.0" +okio = "3.9.0" +retrofit = "2.11.0" +robolectric = "4.12.2" +sparkbutton = "4.2.0" +touchimageview = "3.6" +truth = "1.4.2" +turbine = "1.1.0" +unified-push = "2.4.0" +xmlwriter = "1.0.4" + +[plugins] +android-application = { id = "com.android.application", version.ref = "agp" } +google-ksp = "com.google.devtools.ksp:2.0.0-1.0.22" +hilt-android = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } +kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } +ktlint = "org.jlleitschuh.gradle.ktlint:12.1.1" + +[libraries] +android-material = { module = "com.google.android.material:material", version.ref = "material" } +androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } +androidx-browser = { module = "androidx.browser:browser", version.ref = "androidx-browser" } +androidx-cardview = { module = "androidx.cardview:cardview", version.ref = "androidx-cardview" } +androidx-constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "androidx-constraintlayout" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } +androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "androidx-splashscreen" } +androidx-core-testing = { module = "androidx.arch.core:core-testing", version.ref = "androidx-testing" } +androidx-drawerlayout = { group = "androidx.drawerlayout", name = "drawerlayout", version.ref = "androidx-drawerlayout" } +androidx-emoji2-core = { module = "androidx.emoji2:emoji2", version.ref = "emoji2" } +androidx-emoji2-views-core = { module = "androidx.emoji2:emoji2-views", version.ref = "emoji2" } +androidx-emoji2-view-helper = { module = "androidx.emoji2:emoji2-views-helper", version.ref = "emoji2" } +androidx-exifinterface = { module = "androidx.exifinterface:exifinterface", version.ref = "androidx-exifinterface" } +androidx-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version.ref = "androidx-fragment" } +androidx-hilt-compiler = { module = "androidx.hilt:hilt-compiler", version.ref = "androidx-hilt" } +androidx-hilt-work = { module = "androidx.hilt:hilt-work", version.ref = "androidx-hilt" } +androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "androidx-lifecycle" } +androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "androidx-media3" } +androidx-media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "androidx-media3" } +androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "androidx-media3" } +androidx-paging-runtime-ktx = { module = "androidx.paging:paging-runtime-ktx", version.ref = "androidx-paging" } +androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "androidx-preference" } +androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidx-room" } +androidx-room-paging = { module = "androidx.room:room-paging", version.ref = "androidx-room" } +androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidx-room" } +androidx-room-testing = { module = "androidx.room:room-testing", version.ref = "androidx-room" } +androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "androidx-recyclerview" } +androidx-sharetarget = { module = "androidx.sharetarget:sharetarget", version.ref = "androidx-sharetarget" } +androidx-swiperefreshlayout = { module = "androidx.swiperefreshlayout:swiperefreshlayout", version.ref = "androidx-swiperefresh-layout" } +androidx-test-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-junit" } +androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "androidx-viewpager2" } +androidx-work-runtime-ktx = { module = "androidx.work:work-runtime-ktx", version.ref = "androidx-work" } +androidx-work-testing = { module = "androidx.work:work-testing", version.ref = "androidx-work" } +bouncycastle = { module = "org.bouncycastle:bcprov-jdk15on", version.ref = "bouncycastle" } +conscrypt-android = { module = "org.conscrypt:conscrypt-android", version.ref = "conscrypt" } +hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } +hilt-compiler = { group = "com.google.dagger", name = "hilt-compiler", version.ref = "hilt" } +diffx = { module = "org.pageseeder.diffx:pso-diffx", version.ref = "diffx" } +espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espresso" } +filemojicompat-core = { module = "de.c1710:filemojicompat", version.ref = "filemoji-compat" } +filemojicompat-defaults = { module = "de.c1710:filemojicompat-defaults", version.ref = "filemoji-compat" } +filemojicompat-ui = { module = "de.c1710:filemojicompat-ui", version.ref = "filemoji-compat" } +glide-animation-plugin = { module = "com.github.penfeizhou.android.animation:glide-plugin", version.ref = "glide-animation-plugin" } +glide-compiler = { module = "com.github.bumptech.glide:ksp", version.ref = "glide" } +glide-core = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } +glide-okhttp3-integration = { module = "com.github.bumptech.glide:okhttp3-integration", version.ref = "glide" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } +image-cropper = { module = "com.github.CanHub:Android-Image-Cropper", version.ref = "image-cropper" } +material-drawer-core = { module = "com.mikepenz:materialdrawer", version.ref = "material-drawer" } +material-drawer-iconics = { module = "com.mikepenz:materialdrawer-iconics", version.ref = "material-drawer" } +material-typeface = { module = "com.mikepenz:google-material-typeface", version.ref = "material-typeface" } +mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version.ref = "mockito-kotlin" } +mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockito-inline" } +mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } +moshi-core = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } +moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshi" } +moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" } +networkresult-calladapter = { module = "at.connyduck:networkresult-calladapter", version.ref = "networkresult-calladapter" } +okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } +okhttp-logging-interceptor = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp" } +okio = { module = "com.squareup.okio:okio", version.ref = "okio" } +retrofit-converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "retrofit" } +retrofit-core = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" } +robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +sparkbutton = { module = "at.connyduck.sparkbutton:sparkbutton", version.ref = "sparkbutton" } +touchimageview = { module = "com.github.MikeOrtiz:TouchImageView", version.ref = "touchimageview" } +truth = { module = "com.google.truth:truth", version.ref = "truth" } +turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" } +unified-push = { module = "com.github.UnifiedPush:android-connector", version.ref = "unified-push" } +xmlwriter = { module = "org.pageseeder.xmlwriter:pso-xmlwriter", version.ref = "xmlwriter" } + +[bundles] +androidx = ["androidx-core-ktx", "androidx-appcompat", "androidx-fragment-ktx", "androidx-browser", "androidx-swiperefreshlayout", + "androidx-recyclerview", "androidx-exifinterface", "androidx-cardview", "androidx-preference-ktx", "androidx-sharetarget", + "androidx-emoji2-core", "androidx-emoji2-views-core", "androidx-emoji2-view-helper", "androidx-lifecycle-viewmodel-ktx", + "androidx-constraintlayout", "androidx-paging-runtime-ktx", "androidx-viewpager2", "androidx-work-runtime-ktx", + "androidx-core-splashscreen", "androidx-activity", "androidx-media3-exoplayer", "androidx-media3-datasource-okhttp", + "androidx-media3-ui", "androidx-drawerlayout"] +filemojicompat = ["filemojicompat-core", "filemojicompat-ui", "filemojicompat-defaults"] +glide = ["glide-core", "glide-okhttp3-integration", "glide-animation-plugin"] +material-drawer = ["material-drawer-core", "material-drawer-iconics"] +mockito = ["mockito-kotlin", "mockito-inline"] +moshi = ["moshi-core", "moshi-adapters"] +okhttp = ["okhttp-core", "okhttp-logging-interceptor"] +retrofit = ["retrofit-core", "retrofit-converter-moshi"] +room = ["androidx-room-ktx", "androidx-room-paging"] +xmldiff = ["diffx", "xmlwriter"] diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml new file mode 100644 index 0000000..364f357 --- /dev/null +++ b/gradle/verification-metadata.xml @@ -0,0 +1,6215 @@ +<?xml version="1.0" encoding="UTF-8"?> +<verification-metadata xmlns="https://schema.gradle.org/dependency-verification" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://schema.gradle.org/dependency-verification https://schema.gradle.org/dependency-verification/dependency-verification-1.3.xsd"> + <configuration> + <verify-metadata>true</verify-metadata> + <verify-signatures>false</verify-signatures> + <trusted-artifacts> + <trust file=".*-javadoc[.]jar" regex="true"/> + <trust file=".*-sources[.]jar" regex="true"/> + <trust file="gradle-[0-9.]+-src.zip" regex="true"/> + <trust file="groovy-[a-z]*-?[0-9.]+.pom" regex="true"/> + </trusted-artifacts> + </configuration> + <components> + <component group="androidx.activity" name="activity" version="1.9.0"> + <artifact name="activity-1.9.0-sources.jar"> + <sha256 value="3b95a45ba13704e4872ba0f2755b13ab7013e7f7c08bb8a208417a2b66622e74" origin="Generated by Gradle"/> + </artifact> + <artifact name="activity-1.9.0.aar"> + <sha256 value="46fc8e842d9a4e030dfd9e108c3bc08310f922bd7421f29f67dcaaa4adac3764" origin="Generated by Gradle"/> + </artifact> + <artifact name="activity-1.9.0.module"> + <sha256 value="0750e6ebaaeee0fe793eb51a73691df0ddfa68b543e53e714be6e46c37f6a404" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.activity" name="activity-ktx" version="1.8.1"> + <artifact name="activity-ktx-1.8.1.module"> + <sha256 value="bea2f811b8d8c1b8a835e11d3c68219097c597c458429d3ceafd728e0ca2c6d1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.activity" name="activity-ktx" version="1.9.0"> + <artifact name="activity-ktx-1.9.0.aar"> + <sha256 value="290798b88ce24c897747f15cd25102005a941a2cf4d35efcb8ce2f162c21f589" origin="Generated by Gradle"/> + </artifact> + <artifact name="activity-ktx-1.9.0.module"> + <sha256 value="2ee9817c674857b18c073558c9126a836126c8b046bc3cfda6d638efe8857cad" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.annotation" name="annotation" version="1.0.0"> + <artifact name="annotation-1.0.0.jar"> + <sha256 value="0baae9755f7caf52aa80cd04324b91ba93af55d4d1d17dcc9a7b53d99ef7c016" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotation-1.0.0.pom"> + <sha256 value="a179c12db43d9c0300c9db63f4811db496504be5401b951d422b78490ad1e5b4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.annotation" name="annotation" version="1.1.0"> + <artifact name="annotation-1.1.0.pom"> + <sha256 value="2e9372ba7780ef44952adbf86b66e1f08682c1e5277c926185f6564a13799efe" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.annotation" name="annotation" version="1.2.0"> + <artifact name="annotation-1.2.0.jar"> + <sha256 value="9029262bddce116e6d02be499e4afdba21f24c239087b76b3b57d7e98b490a36" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotation-1.2.0.module"> + <sha256 value="2efcab81ef91b211bacd206eaacd995a51f633a2e96b57a8fc00144c5f9c56b3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.annotation" name="annotation" version="1.3.0"> + <artifact name="annotation-1.3.0.jar"> + <sha256 value="97dc45afefe3a1e421da42b8b6e9f90491477c45fc6178203e3a5e8a05ee8553" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotation-1.3.0.module"> + <sha256 value="9516c2ae44284ea0bd3d0eade0ee638879b708cbe31e3af92ba96c300604ebc3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.annotation" name="annotation" version="1.5.0"> + <artifact name="annotation-1.5.0-sources.jar"> + <sha256 value="321c0d46007a21b02c99b8b690afb9eec200c7fde0f60ab7e056c1d0c3024d42" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotation-1.5.0.module"> + <sha256 value="4c84feee2db891ff6b97d613a0d40ab96ce297b034a6927ca8479f09e82d7c2e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.annotation" name="annotation" version="1.6.0"> + <artifact name="annotation-1.6.0.module"> + <sha256 value="6146b6138643b2ac0590df509dd51abaea769c79fd7602eb217168fe5af78cd2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.annotation" name="annotation" version="1.8.0"> + <artifact name="annotation-1.8.0.module"> + <sha256 value="d590a0d8e02f405de749e8dc80b741dc503c6e3e4c9c016d614d76b65f0b59ef" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotation-metadata-1.8.0.jar"> + <sha256 value="fe70ace6f942a5fc29045bb2fe25b4e77bdc742dc69f76ed65b39c3ae185888e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.annotation" name="annotation-experimental" version="1.0.0"> + <artifact name="annotation-experimental-1.0.0.pom"> + <sha256 value="6b73ff6608f4b1d6cbab620b65708a382d0b39901cf4e6b0d16f84a1b04d7732" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.annotation" name="annotation-experimental" version="1.1.0"> + <artifact name="annotation-experimental-1.1.0.aar"> + <sha256 value="0157de61a2064047896a058080f3fd67ba57ad9a94857b3f7a363660243e3f90" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotation-experimental-1.1.0.module"> + <sha256 value="0361d1526a4d7501255e19779e09e93cdbd07fee0e2f5c50b7a137432d510119" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.annotation" name="annotation-experimental" version="1.3.1"> + <artifact name="annotation-experimental-1.3.1-sources.jar"> + <sha256 value="d23e1a3f75f5cb1b45f900eb1a5bcc6accc2db841a4d3d9d334754e73ae60b2c" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotation-experimental-1.3.1.module"> + <sha256 value="9b6974a7dfe26d3c209dd63e16f8ee2461b57a091789160ca1eb492bb1bf3f84" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.annotation" name="annotation-experimental" version="1.4.0"> + <artifact name="annotation-experimental-1.4.0.aar"> + <sha256 value="c6eb7e676011ec65b32428373d450debdfc45179c4f8b3a752174fb87c17b08a" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotation-experimental-1.4.0.module"> + <sha256 value="5930ea7f21fcb6d0deb2ba32748a0ef7c8fd2c42384860582ba7cd20deb90379" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.annotation" name="annotation-jvm" version="1.6.0"> + <artifact name="annotation-jvm-1.6.0.module"> + <sha256 value="3f5a8faa19de667e63dca9730ff8ef0e478e4bafb5feeb8258e5c086246dc90c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.annotation" name="annotation-jvm" version="1.8.0"> + <artifact name="annotation-jvm-1.8.0.jar"> + <sha256 value="9aab326d9492800991854360ac248f493ce7f7c3183519309b78ace9e240f6f6" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotation-jvm-1.8.0.module"> + <sha256 value="e3cb4525539d0ed74bb238ef92c69eef22a80e422c0d2acbc51e6187febb0a13" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.appcompat" name="appcompat" version="1.7.0"> + <artifact name="appcompat-1.7.0-sources.jar"> + <sha256 value="6e3d3af46fbd9cb51ce6feb791274a9c2da11b64ba7b967a5dd12915e0aa031a" origin="Generated by Gradle"/> + </artifact> + <artifact name="appcompat-1.7.0.aar"> + <sha256 value="67189713b30a3fab6971713cc5fab1cb7f022bcf648a257563715c91d719d584" origin="Generated by Gradle"/> + </artifact> + <artifact name="appcompat-1.7.0.module"> + <sha256 value="38842af2b96c99540cc756337754b3030de08b029d4bf3aa06e4ecf741da68bf" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.appcompat" name="appcompat-resources" version="1.7.0"> + <artifact name="appcompat-resources-1.7.0.aar"> + <sha256 value="55b6778602680f3c288ce350a2c2d3dd158d97dbffc63476275826655582c388" origin="Generated by Gradle"/> + </artifact> + <artifact name="appcompat-resources-1.7.0.module"> + <sha256 value="d7cca0b553ec109ef20ac70ae6438f584fc6bb5eb269a7f5b4098e02c6d687f9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.arch.core" name="core-common" version="2.1.0"> + <artifact name="core-common-2.1.0.pom"> + <sha256 value="83bbb3960eaabc600ac366c94cb59414e441532a1d6aa9388b0b8bfface5cf01" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.arch.core" name="core-common" version="2.2.0"> + <artifact name="core-common-2.2.0.jar"> + <sha256 value="65308a06b1c00ee186cb9e19321383f043b993813f1522c47f4a3e3303bdba41" origin="Generated by Gradle"/> + </artifact> + <artifact name="core-common-2.2.0.module"> + <sha256 value="edf4200cfdc2d946232252c99e5dcb9c61bb909eb5450b2613d1d4fdc974b981" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.arch.core" name="core-runtime" version="2.2.0"> + <artifact name="core-runtime-2.2.0.aar"> + <sha256 value="a1be5e0caa2b07623862af6ae21b3ab0718123245184d0e30dea81b53f990a47" origin="Generated by Gradle"/> + </artifact> + <artifact name="core-runtime-2.2.0.module"> + <sha256 value="a8b17513949e5db6c9601c30be19df953762dd877512f1e2cfcfae81d2440944" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.arch.core" name="core-testing" version="2.2.0"> + <artifact name="core-testing-2.2.0-sources.jar"> + <sha256 value="0ded176aa10334727f36285385b28bf7cfd897dd49f3c841382b8bee2e2b7897" origin="Generated by Gradle"/> + </artifact> + <artifact name="core-testing-2.2.0.aar"> + <sha256 value="85e1fe770ed673ecb8552eaadc23b96d8db0a14a10eaa7789518496e5d99362c" origin="Generated by Gradle"/> + </artifact> + <artifact name="core-testing-2.2.0.module"> + <sha256 value="3f4af097f3c6e5211b6716560ee25fce21c74e6c79ace9bc5241e20f75fef2a3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.browser" name="browser" version="1.8.0"> + <artifact name="browser-1.8.0-sources.jar"> + <sha256 value="6097e75478d6caf2afbe145c99f1df74ced667d5b7c7646663955ac43aeadf7a" origin="Generated by Gradle"/> + </artifact> + <artifact name="browser-1.8.0.aar"> + <sha256 value="2d6167b33bf6a064c37501501543702349e850ac62c732d1efa4e71063675456" origin="Generated by Gradle"/> + </artifact> + <artifact name="browser-1.8.0.module"> + <sha256 value="b13a2b7c64c0f2f349a19df512360ed917a63f894fc3de297a1fde4fb287546c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.cardview" name="cardview" version="1.0.0"> + <artifact name="cardview-1.0.0-sources.jar"> + <sha256 value="ad7a28e7768893f8b33a8b7b275a92e33c125243ca4a9130a984c40e578e19b4" origin="Generated by Gradle"/> + </artifact> + <artifact name="cardview-1.0.0.aar"> + <sha256 value="1193c04c22a3d6b5946dae9f4e8c59d6adde6a71b6bd5d87fb99d82dda1afec7" origin="Generated by Gradle"/> + </artifact> + <artifact name="cardview-1.0.0.pom"> + <sha256 value="e64ef4e08b58358fe27b599e6fe80a1b153db014c644beee630ab271061c3e6c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.collection" name="collection" version="1.0.0"> + <artifact name="collection-1.0.0.pom"> + <sha256 value="a7913a5275ad68e555d2612ebe8c14c367b153e14ca48a1872a64899020e54ef" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.collection" name="collection" version="1.1.0"> + <artifact name="collection-1.1.0-sources.jar"> + <sha256 value="158ae7efee9c7394a241139ebf220751f8b812eda40269a38ef725dbe784b98d" origin="Generated by Gradle"/> + </artifact> + <artifact name="collection-1.1.0.jar"> + <sha256 value="632a0e5407461de774409352940e292a291037724207a787820c77daf7d33b72" origin="Generated by Gradle"/> + </artifact> + <artifact name="collection-1.1.0.pom"> + <sha256 value="67e9066ca4acfdc6e3cc508293c31ba0398057ff118e4f70b1e1813c9a3456d1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.collection" name="collection" version="1.2.0"> + <artifact name="collection-1.2.0.module"> + <sha256 value="6c4c0f9e7dab6d983858e67dd99a93af9d9f7d02ac1f8f648ce1fecc84afffa2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.collection" name="collection" version="1.4.0"> + <artifact name="collection-1.4.0.module"> + <sha256 value="2fd3b523e8276c0254c417b66d8e7fecc5b0b975af5d178afa7f5b813812cfab" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.collection" name="collection-jvm" version="1.4.0"> + <artifact name="collection-jvm-1.4.0.jar"> + <sha256 value="d5cf7b72647c7995071588fe870450ff9c8f127f253d2d4851e161b800f67ae0" origin="Generated by Gradle"/> + </artifact> + <artifact name="collection-jvm-1.4.0.module"> + <sha256 value="21b0b02ea68abe418f3dd4e4d42876ecf3bd9a1cada458b460cae1cd9d58ef6d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.collection" name="collection-ktx" version="1.1.0"> + <artifact name="collection-ktx-1.1.0.jar"> + <sha256 value="2bfc54475c047131913361f56d0f7f019c6e5bee53eeb0eb7d94a7c499a05227" origin="Generated by Gradle"/> + </artifact> + <artifact name="collection-ktx-1.1.0.pom"> + <sha256 value="721e76e74ee4158d3fe9759074b7eceed4ff7d84ed34a3faca5843fb874ac946" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.collection" name="collection-ktx" version="1.4.0"> + <artifact name="collection-ktx-1.4.0.jar"> + <sha256 value="c6deada2fac53b8ea6523dbda77597b128006674616f140f04df23264c6d1aa3" origin="Generated by Gradle"/> + </artifact> + <artifact name="collection-ktx-1.4.0.module"> + <sha256 value="3770999ec32d1c082d1a34cf1d64d16d9eca9b6b1263c979954407f461fba82b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.compose.runtime" name="runtime" version="1.6.7"> + <artifact name="runtime-1.6.7.module"> + <sha256 value="2922f1c5cd195b2d852461cde71c0ed36877e4da5b5cd7cacceb3042fc2604ea" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.compose.runtime" name="runtime-android" version="1.6.7"> + <artifact name="runtime-android-1.6.7.module"> + <sha256 value="c05681960b711dc58160ca439e416819664e9fbdc3d681e977a7944a504dce02" origin="Generated by Gradle"/> + </artifact> + <artifact name="runtime-release.aar"> + <sha256 value="096fc94de01f27bc783bb0b8984b49aeda688f6329f8393d42d4b6754beae7da" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.concurrent" name="concurrent-futures" version="1.0.0"> + <artifact name="concurrent-futures-1.0.0.pom"> + <sha256 value="4505b9a5e30a9418b59a9ad6702c3e4193aea6e691a3d03cf220c7640ad083e2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.concurrent" name="concurrent-futures" version="1.1.0"> + <artifact name="concurrent-futures-1.1.0-sources.jar"> + <sha256 value="cc06fb6045a6c066f50e237a3c2126bc88c16d358a57b3612ba633c9f530b0a0" origin="Generated by Gradle"/> + </artifact> + <artifact name="concurrent-futures-1.1.0.jar"> + <sha256 value="0ce067c514a0d1049d1bebdf709e344ed3266fe9744275682937cdcb13334e9e" origin="Generated by Gradle"/> + </artifact> + <artifact name="concurrent-futures-1.1.0.module"> + <sha256 value="77639a0b051e22510bad93affcea0ebd781ef124bf9b7621a95749937bcfcdfd" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.constraintlayout" name="constraintlayout" version="2.1.4"> + <artifact name="constraintlayout-2.1.4.aar"> + <sha256 value="0df714c0b51e54710ebf746eb469d333176bbb3cb29f80775dc3ca4eb3162512" origin="Generated by Gradle"/> + </artifact> + <artifact name="constraintlayout-2.1.4.module"> + <sha256 value="1fd15b84220cf35693f57b235b84b9dd70f4c31ca8ad396383dec5a288d9df96" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.constraintlayout" name="constraintlayout-core" version="1.0.4"> + <artifact name="constraintlayout-core-1.0.4.jar"> + <sha256 value="3e477f4de231e58b25f5a992f3be45e97d332c34a39a9e3e7d4b78ae0ac2256f" origin="Generated by Gradle"/> + </artifact> + <artifact name="constraintlayout-core-1.0.4.module"> + <sha256 value="7cfe8d755d524185204c68fe512431aafa5487553adcf2356eec59eab9c64185" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.coordinatorlayout" name="coordinatorlayout" version="1.1.0"> + <artifact name="coordinatorlayout-1.1.0.aar"> + <sha256 value="44a9e30abf56af1025c52a0af506fee9c4131aa55efda52f9fd9451211c5e8cb" origin="Generated by Gradle"/> + </artifact> + <artifact name="coordinatorlayout-1.1.0.pom"> + <sha256 value="a67c52c9ddfaff2ffb2fd4b97cd94fa382e837ea8a5874d029e0a04fa63e5caf" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.core" name="core" version="1.1.0"> + <artifact name="core-1.1.0.aar"> + <sha256 value="76c7cfbe596fe3c09a6983bf1c89e889299c08ac9a3b52ce5182a088d056647e" origin="Generated by Gradle"/> + </artifact> + <artifact name="core-1.1.0.pom"> + <sha256 value="dae46132cdcd46b798425f7cb78fd65890869b6d26101ccdcd43461a4f51754c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.core" name="core" version="1.13.1"> + <artifact name="core-1.13.1.aar"> + <sha256 value="2c27de199535675005553066597a4b20fa1eea7c228ab4ef6b32b5fe39ca1f59" origin="Generated by Gradle"/> + </artifact> + <artifact name="core-1.13.1.module"> + <sha256 value="2a10979bbb3bcd7b25b7f664ab4e9b016fabf2174a26768b677e12e4bea4c7c4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.core" name="core-ktx" version="1.13.1"> + <artifact name="core-ktx-1.13.1-sources.jar"> + <sha256 value="b6646f76cd2d59210561dffb16b32a4e5fcca6de37ca027f1dae82bfb0cf6493" origin="Generated by Gradle"/> + </artifact> + <artifact name="core-ktx-1.13.1.aar"> + <sha256 value="19ba50d094c7368ede1b4ccf1195ceb83e35970736593f823e5af716f8d05d70" origin="Generated by Gradle"/> + </artifact> + <artifact name="core-ktx-1.13.1.module"> + <sha256 value="ab530b04e2fbe73205484899a3be56c90968873eb496fc52e5ea7596d91035d1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.core" name="core-splashscreen" version="1.2.0-alpha01"> + <artifact name="core-splashscreen-1.2.0-alpha01-sources.jar"> + <sha256 value="5e82448d6f1c3aad499ff0f5f9d24602f880eb8d26fb13525821a3669b3466de" origin="Generated by Gradle"/> + </artifact> + <artifact name="core-splashscreen-1.2.0-alpha01.aar"> + <sha256 value="d750a1bc1f3f6c4af748ff49eb4cbcacb3c22257c935f8076762508d1aa740f8" origin="Generated by Gradle"/> + </artifact> + <artifact name="core-splashscreen-1.2.0-alpha01.module"> + <sha256 value="dc11ea4e05d403739592d854f833860df7321d7f4262b40463ee54f51bce0107" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.cursoradapter" name="cursoradapter" version="1.0.0"> + <artifact name="cursoradapter-1.0.0.aar"> + <sha256 value="a81c8fe78815fa47df5b749deb52727ad11f9397da58b16017f4eb2c11e28564" origin="Generated by Gradle"/> + </artifact> + <artifact name="cursoradapter-1.0.0.pom"> + <sha256 value="62d95c89850af21030b19f14d5f7ecd6d8bcc9a3014c59002ec99624caac8100" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.customview" name="customview" version="1.0.0"> + <artifact name="customview-1.0.0.pom"> + <sha256 value="ce9e47b87184f5bd5e139e9becd5b26476d42d78c31bf2fdedc37acb41b9ad49" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.customview" name="customview" version="1.1.0"> + <artifact name="customview-1.1.0.aar"> + <sha256 value="01f76ab043770a97b054046f9815717b82ce0355c02967d16c61981359dc189a" origin="Generated by Gradle"/> + </artifact> + <artifact name="customview-1.1.0.pom"> + <sha256 value="c814d435f73e9e6d169886d0eb96b5c5361feb48449fbbb315c908c03c588c94" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.customview" name="customview-poolingcontainer" version="1.0.0"> + <artifact name="customview-poolingcontainer-1.0.0.aar"> + <sha256 value="3584102fc49bf399c56e3b7be4bfe12000c46112320cd8cf85cc0a8f93f3e752" origin="Generated by Gradle"/> + </artifact> + <artifact name="customview-poolingcontainer-1.0.0.module"> + <sha256 value="903034d5152dd2e0162b1468ea25a22e1ca384006b3d282d5a143cc760321a01" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.databinding" name="databinding-common" version="8.4.1"> + <artifact name="databinding-common-8.4.1.jar"> + <sha256 value="66cab82639dac0f6c2433464c093b074d608c4bb887ec38a9b8bc4ac98126732" origin="Generated by Gradle"/> + </artifact> + <artifact name="databinding-common-8.4.1.pom"> + <sha256 value="0c21f50a67ebb4900579a631aba1e122d76a70bd65b23f7ee674aeae5f07f426" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.databinding" name="databinding-common" version="8.4.2"> + <artifact name="databinding-common-8.4.2.jar"> + <sha256 value="66cab82639dac0f6c2433464c093b074d608c4bb887ec38a9b8bc4ac98126732" origin="Generated by Gradle"/> + </artifact> + <artifact name="databinding-common-8.4.2.pom"> + <sha256 value="a711fbc4df49f063b3c54211f9c29c4498cfc99c64c136462049e60e965fb32b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.databinding" name="databinding-common" version="8.5.0"> + <artifact name="databinding-common-8.5.0.jar"> + <sha256 value="66cab82639dac0f6c2433464c093b074d608c4bb887ec38a9b8bc4ac98126732" origin="Generated by Gradle"/> + </artifact> + <artifact name="databinding-common-8.5.0.pom"> + <sha256 value="bf22c17278ec3454e096771f7c17a6ac615e5c630937696e82b6ede1193134d9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.databinding" name="databinding-compiler-common" version="8.4.1"> + <artifact name="databinding-compiler-common-8.4.1.jar"> + <sha256 value="6a774892f0a035329a3160d3b72ae5b1f1b981d481ec55c1d147501ada3f2ffd" origin="Generated by Gradle"/> + </artifact> + <artifact name="databinding-compiler-common-8.4.1.pom"> + <sha256 value="cd2e2065016e5010563053905ff7e5fd161a4b69cb26a32f16ecbb47e1779658" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.databinding" name="databinding-compiler-common" version="8.4.2"> + <artifact name="databinding-compiler-common-8.4.2.jar"> + <sha256 value="d08bf1bd322eccda3f8f71e04ef963a29b65bc040eba01daf1925d0f1e3bf14c" origin="Generated by Gradle"/> + </artifact> + <artifact name="databinding-compiler-common-8.4.2.pom"> + <sha256 value="6b233dc97a746b209261d151bcd9ffcd3da17c4013ab0c361417278965569487" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.databinding" name="databinding-compiler-common" version="8.5.0"> + <artifact name="databinding-compiler-common-8.5.0.jar"> + <sha256 value="0fde528ce412d84bd12890c2bff98c4656446a6bd8340f80ce9cbf48b6b4d160" origin="Generated by Gradle"/> + </artifact> + <artifact name="databinding-compiler-common-8.5.0.pom"> + <sha256 value="ead64e3315f2907c3f5e6e50a6a6e560c2b004aa67fac899cbf446761c208d7c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.databinding" name="viewbinding" version="8.4.1"> + <artifact name="viewbinding-8.4.1.aar"> + <sha256 value="f3d9d1bb36d0a0f4e7cd3cffc369196668664e2e5b1211e912ba1e5587ae7cb8" origin="Generated by Gradle"/> + </artifact> + <artifact name="viewbinding-8.4.1.module"> + <sha256 value="7d6c0745235dac264e21eccde01d0154ff7611b204353c3c0a8672d597aa3f4a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.databinding" name="viewbinding" version="8.4.2"> + <artifact name="viewbinding-8.4.2.aar"> + <sha256 value="9ba75a680a9afc82cd0217940ede9cb53dc9d49bf931ace0ae973186f6b0954a" origin="Generated by Gradle"/> + </artifact> + <artifact name="viewbinding-8.4.2.module"> + <sha256 value="cd4423f1ec2218caacee040e819ecb741c4efa2357e813260f2d48bd2ffbc7d1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.databinding" name="viewbinding" version="8.5.0"> + <artifact name="viewbinding-8.5.0.aar"> + <sha256 value="225e1aeac33a9bdd32ffc5fba4d1c49f5d134e2df83b191d0d03971d0d8f2817" origin="Generated by Gradle"/> + </artifact> + <artifact name="viewbinding-8.5.0.module"> + <sha256 value="a5b4944eb291d6a73f47741e13ff0ea59cca6c48535f92eb0c49a55ad4b4ce5f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.documentfile" name="documentfile" version="1.0.0"> + <artifact name="documentfile-1.0.0.aar"> + <sha256 value="865a061ef2fad16522f8433536b8d47208c46ff7c7745197dfa1eeb481869487" origin="Generated by Gradle"/> + </artifact> + <artifact name="documentfile-1.0.0.pom"> + <sha256 value="013288a9317a552706ce625fb24493e8223288529223ec578cf855a5ae9c16e5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.drawerlayout" name="drawerlayout" version="1.2.0"> + <artifact name="drawerlayout-1.2.0-sources.jar"> + <sha256 value="b491d8d60a6309f572a15c402a3e819ef1b4725711202a7a8eaa8982295eaad5" origin="Generated by Gradle"/> + </artifact> + <artifact name="drawerlayout-1.2.0.aar"> + <sha256 value="f4ba16c51d5182497fe128e298af930f0ec6bd5497c7440a4c67042d15dd7702" origin="Generated by Gradle"/> + </artifact> + <artifact name="drawerlayout-1.2.0.module"> + <sha256 value="b802e0828fa9948f561f01b041abac71278bfe987373a9a495abed9fe5b65035" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.dynamicanimation" name="dynamicanimation" version="1.0.0"> + <artifact name="dynamicanimation-1.0.0.aar"> + <sha256 value="ce005162c229bf308d2d5b12fb6cad0874069cbbeaccee63a8193bd08d40de04" origin="Generated by Gradle"/> + </artifact> + <artifact name="dynamicanimation-1.0.0.pom"> + <sha256 value="44ce22ee620d28f17301bcc60ad49b69b7d0596c2a87b054ad1e3feac7b4a898" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.emoji2" name="emoji2" version="1.4.0"> + <artifact name="emoji2-1.4.0-sources.jar"> + <sha256 value="5db44eec9cee653ba403f0e88a02ce41ab6301a0b7dd7cb35fc32f9747f2228b" origin="Generated by Gradle"/> + </artifact> + <artifact name="emoji2-1.4.0.aar"> + <sha256 value="433febd3434a45667176c76a64f3f205ca6335a6b544c5b5d57f25a38a375242" origin="Generated by Gradle"/> + </artifact> + <artifact name="emoji2-1.4.0.module"> + <sha256 value="92be16cccb757f807bddd92f1fca95a038a810ccca051c4149c11682478954b8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.emoji2" name="emoji2-views" version="1.4.0"> + <artifact name="emoji2-views-1.4.0-sources.jar"> + <sha256 value="77ff753b82443e4584d84a0e80d9e49830f15ea3a28059c39955077d51f3f3b5" origin="Generated by Gradle"/> + </artifact> + <artifact name="emoji2-views-1.4.0.aar"> + <sha256 value="fd2fbc9a678101498ebc21fa75e11a83e2b2743715230eef1dc1a77fa4bf4580" origin="Generated by Gradle"/> + </artifact> + <artifact name="emoji2-views-1.4.0.module"> + <sha256 value="f496cced4c7b3a6854a7ba07b3d3a35226f1056a5359a9f9563a222e72591a42" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.emoji2" name="emoji2-views-helper" version="1.4.0"> + <artifact name="emoji2-views-helper-1.4.0-sources.jar"> + <sha256 value="7c813c4fc93038ef7d262fd894c48f960f6d914f79d83407106ede1ab55b92a0" origin="Generated by Gradle"/> + </artifact> + <artifact name="emoji2-views-helper-1.4.0.aar"> + <sha256 value="ed5d3ed772a5fbf0d570f7526f585cd61a180e60f9372584c328a68e2cff3375" origin="Generated by Gradle"/> + </artifact> + <artifact name="emoji2-views-helper-1.4.0.module"> + <sha256 value="248aa276fdba8116e656233cac3f3fd541444df2305de93e6813795fcd0ba26a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.exifinterface" name="exifinterface" version="1.3.7"> + <artifact name="exifinterface-1.3.7-sources.jar"> + <sha256 value="04c0a6d2ff9745ab9946ebcdd2cf35889e05c79e5218e8b59a57a98a2670dc74" origin="Generated by Gradle"/> + </artifact> + <artifact name="exifinterface-1.3.7.aar"> + <sha256 value="0e8f1832266c5b0667ad3d3b1098e624e49a09075493a014a7e88af01fd30ad3" origin="Generated by Gradle"/> + </artifact> + <artifact name="exifinterface-1.3.7.module"> + <sha256 value="f70819519ecc320028345c7c0c318d8157055e83fc466c899cd344dd4f630a31" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.fragment" name="fragment" version="1.5.4"> + <artifact name="fragment-1.5.4.module"> + <sha256 value="af3260808dceb6532efc2d7215be45872c24a699dada7d77bff738ce3b85a7f0" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.fragment" name="fragment" version="1.7.1"> + <artifact name="fragment-1.7.1-sources.jar"> + <sha256 value="e1f221e82d5d549dfc50151b5f80aab86dd62a3db71a5ca0b0a3bbabf91765df" origin="Generated by Gradle"/> + </artifact> + <artifact name="fragment-1.7.1.aar"> + <sha256 value="7ddebe9d830a04963733dcb340cab4bac1e69dfca797a6d59aa7bc56282150af" origin="Generated by Gradle"/> + </artifact> + <artifact name="fragment-1.7.1.module"> + <sha256 value="d6aea759c6903c44da1cbe5f770d5ed3cad0b38945535089bcfacfe705f57841" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.fragment" name="fragment" version="1.8.0"> + <artifact name="fragment-1.8.0-sources.jar"> + <sha256 value="dadc27ff90ff01cbe177b309fbbf105a1f7a154d52e716b0e9f93edb0fc18b6a" origin="Generated by Gradle"/> + </artifact> + <artifact name="fragment-1.8.0.aar"> + <sha256 value="7ff14d11a3167a854ac12e62aa6b5a68d230821664199244d1563cf30b5c6fd0" origin="Generated by Gradle"/> + </artifact> + <artifact name="fragment-1.8.0.module"> + <sha256 value="6aff4179065b190ab32f9d6439add487aaeabc502bb07df0cddae63aade1b62b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.fragment" name="fragment-ktx" version="1.7.1"> + <artifact name="fragment-ktx-1.7.1-sources.jar"> + <sha256 value="6c9da4629e5057aa9f7caf9c2f6f77da387b0dec681d3dd65662d33c0871f7ca" origin="Generated by Gradle"/> + </artifact> + <artifact name="fragment-ktx-1.7.1.aar"> + <sha256 value="8facb90718cf525eaffe6d1bd6145dff156d6ae51d3c1f8202cd058ef881f62a" origin="Generated by Gradle"/> + </artifact> + <artifact name="fragment-ktx-1.7.1.module"> + <sha256 value="8cc96d948489f9411c91796e5c7332ecfb070c2d0f4d65b7dc3e7978fa255bbd" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.fragment" name="fragment-ktx" version="1.8.0"> + <artifact name="fragment-ktx-1.8.0-sources.jar"> + <sha256 value="6c9da4629e5057aa9f7caf9c2f6f77da387b0dec681d3dd65662d33c0871f7ca" origin="Generated by Gradle"/> + </artifact> + <artifact name="fragment-ktx-1.8.0.aar"> + <sha256 value="29b4003b3a4639aeb5df989c85aefbf861a83ac18493a1defb71c5c191b0afd5" origin="Generated by Gradle"/> + </artifact> + <artifact name="fragment-ktx-1.8.0.module"> + <sha256 value="76993ac6f4d410942f4355f7cf45d56a0f465c08a1dbbd71bb30ad03b082cefc" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.hilt" name="hilt-common" version="1.2.0"> + <artifact name="hilt-common-1.2.0.jar"> + <sha256 value="6b09081fd5a10de569b4be6463b146f6045bb18919bf309aff064980b6310371" origin="Generated by Gradle"/> + </artifact> + <artifact name="hilt-common-1.2.0.module"> + <sha256 value="285d6db73ad92ed742b32251bac75314d08e68c93aaee46d9dd5092e93b48ce9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.hilt" name="hilt-compiler" version="1.2.0"> + <artifact name="hilt-compiler-1.2.0.jar"> + <sha256 value="4369f2d0898ed1fa9d439d21655c964e15d13a3731dee893508ab5fad3a30be8" origin="Generated by Gradle"/> + </artifact> + <artifact name="hilt-compiler-1.2.0.module"> + <sha256 value="89c0b0a0e41c7948fe93933456ff115a8f1e196726096ee4f796af1a58bc7371" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.hilt" name="hilt-work" version="1.2.0"> + <artifact name="hilt-work-1.2.0-sources.jar"> + <sha256 value="e47952e0207c7011734ee0901a329e84da31942f7f45f761a1d8660e6b2a8b2e" origin="Generated by Gradle"/> + </artifact> + <artifact name="hilt-work-1.2.0.aar"> + <sha256 value="8f02b962a84b686e4a4749db40ec884d97a7439ff9191a9a26e46e69879e4bf8" origin="Generated by Gradle"/> + </artifact> + <artifact name="hilt-work-1.2.0.module"> + <sha256 value="fed81bb6537730631d9ff5344a3cdb184a43d742d6c68b79bec611bb03f63b67" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.interpolator" name="interpolator" version="1.0.0"> + <artifact name="interpolator-1.0.0.aar"> + <sha256 value="33193135a64fe21fa2c35eec6688f1a76e512606c0fc83dc1b689e37add7732a" origin="Generated by Gradle"/> + </artifact> + <artifact name="interpolator-1.0.0.pom"> + <sha256 value="0ddc07cc39699f48ecd9ec894b5830c0f09e22e82959294edf37217224c88b7b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.legacy" name="legacy-support-core-utils" version="1.0.0"> + <artifact name="legacy-support-core-utils-1.0.0.aar"> + <sha256 value="a7edcf01d5b52b3034073027bc4775b78a4764bb6202bb91d61c829add8dd1c7" origin="Generated by Gradle"/> + </artifact> + <artifact name="legacy-support-core-utils-1.0.0.pom"> + <sha256 value="8fd093008b3ee7c06e52c78da2af980a7b47b69b967fa91dad7af466f7a00a38" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-common" version="2.3.1"> + <artifact name="lifecycle-common-2.3.1.jar"> + <sha256 value="15848fb56db32f4c7cdc72b324003183d52a4884d6bf09be708ac7f587d139b5" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-common-2.3.1.module"> + <sha256 value="5fb7c8514d8c56cada5e29ef89dc0289e71942ab4cb0b2e6dca137b9dcb8fdd4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-common" version="2.5.1"> + <artifact name="lifecycle-common-2.5.1.jar"> + <sha256 value="20ad1520f625cf455e6afd7290988306d3a9886efa993e0860fbabf4bb3f7bda" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-common-2.5.1.module"> + <sha256 value="7d4bc2961cd5bd399e3621d434f0c453dd6cadf891f917a946cc291abdda8f1a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-common" version="2.8.1"> + <artifact name="lifecycle-common-2.8.1.module"> + <sha256 value="4442d73add4e6862086c8f979be651c0fa24f3a61f3e30368daaa9ce99fc57d6" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-common-metadata-2.8.1.jar"> + <sha256 value="e26c6d7d04a490c6fdaa1415ac95797a7dcc7444be3f3ab3d9acad87dae4921c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-common" version="2.8.2"> + <artifact name="lifecycle-common-2.8.2.module"> + <sha256 value="bbf1250f00552c82871e283dc2af380b1bc2a1b52a207f012f5d8c849544b621" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-common-metadata-2.8.2.jar"> + <sha256 value="e26c6d7d04a490c6fdaa1415ac95797a7dcc7444be3f3ab3d9acad87dae4921c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-common-java8" version="2.8.1"> + <artifact name="lifecycle-common-java8-2.8.1.jar"> + <sha256 value="c6deada2fac53b8ea6523dbda77597b128006674616f140f04df23264c6d1aa3" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-common-java8-2.8.1.module"> + <sha256 value="805ef2c1224fc13d426e586d37673b26c23e7b15593109d214306ea246aa86b0" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-common-java8" version="2.8.2"> + <artifact name="lifecycle-common-java8-2.8.2.jar"> + <sha256 value="c6deada2fac53b8ea6523dbda77597b128006674616f140f04df23264c6d1aa3" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-common-java8-2.8.2.module"> + <sha256 value="391ec077deaa358a4955d6e63c6171d4bd514c46705d9b1972b9bd33f1d2e4dc" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-common-jvm" version="2.8.1"> + <artifact name="lifecycle-common-jvm-2.8.1-sources.jar"> + <sha256 value="99aa4aa1f4432d9dab6a8b07ac01c6c7eb0c0f813f4c727bc29e9e60a0059480" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-common-jvm-2.8.1.jar"> + <sha256 value="61c873a7327c946ec033c310bb98f3f92eeabcede0e1a5200ab8a1896483c7bf" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-common-jvm-2.8.1.module"> + <sha256 value="f9d3ee151ba951be2df50a9a45f237d8394bff2968edcc11e1014b8894583cbb" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-common-jvm" version="2.8.2"> + <artifact name="lifecycle-common-jvm-2.8.2-sources.jar"> + <sha256 value="99aa4aa1f4432d9dab6a8b07ac01c6c7eb0c0f813f4c727bc29e9e60a0059480" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-common-jvm-2.8.2.jar"> + <sha256 value="61c873a7327c946ec033c310bb98f3f92eeabcede0e1a5200ab8a1896483c7bf" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-common-jvm-2.8.2.module"> + <sha256 value="46b20ea622920f2c083a1f96efb983a4a16823676daed7478aa6fce5e88c3a52" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-livedata" version="2.5.1"> + <artifact name="lifecycle-livedata-2.5.1.aar"> + <sha256 value="8ad18cf18a8f82d77b11aab49cf9b9b3d418e5f564b216e91d815cf038cefdfb" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-livedata-2.5.1.module"> + <sha256 value="b5f4a08193d7802ac3574d91ef442ae31c633ff3095f1c4973c80d68908a48bc" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-livedata" version="2.8.1"> + <artifact name="lifecycle-livedata-2.8.1.aar"> + <sha256 value="1a280b5d6351515907aaa19d3179ec8076095985e979e323a5972bdc5d6b7945" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-livedata-2.8.1.module"> + <sha256 value="5e7fb36c1c4ce810bfaa3d41cd3ff44b86439c04e6c27cbbb209996cb6573e40" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-livedata" version="2.8.2"> + <artifact name="lifecycle-livedata-2.8.2.aar"> + <sha256 value="5b01d448a59caf922c5e9d9e9d1067f25a2b015fd5130e9baac9c86ed2464354" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-livedata-2.8.2.module"> + <sha256 value="f36fd960cae98fce668cf98ea57a33af120cdee0cfb275e94fca74663a4918f6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-livedata-core" version="2.5.1"> + <artifact name="lifecycle-livedata-core-2.5.1.aar"> + <sha256 value="ee792103ca248bfaf150c45a93871e4cf7e8cebab990e0f62f7de5d4ff2f209f" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-livedata-core-2.5.1.module"> + <sha256 value="3f388e9e078901970c2bfcfc02fecae948de4b46be5211919ae07d012ca2980d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-livedata-core" version="2.8.1"> + <artifact name="lifecycle-livedata-core-2.8.1.aar"> + <sha256 value="ed204afc11b8db2b154bee54a2b1af28d46acc54dc05dce463b4fc61e3ee3995" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-livedata-core-2.8.1.module"> + <sha256 value="5e22f4a4769c06bb86da098a6d38fda8e625b2d656f1d0cd7144a90f8f018f4c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-livedata-core" version="2.8.2"> + <artifact name="lifecycle-livedata-core-2.8.2.aar"> + <sha256 value="906199de52b28505def4ad5e5a250286e239cd858ef8007843b80754754bf4e2" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-livedata-core-2.8.2.module"> + <sha256 value="f386a0f6bee8f0379c646989a45f93bf5cd1bfa4743690a6c62654a37f2f020a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-livedata-core-ktx" version="2.6.1"> + <artifact name="lifecycle-livedata-core-ktx-2.6.1.module"> + <sha256 value="d8699c515516aee3a0613de951649d9f1e137d33fedf077be0a6aa9e01ae211f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-livedata-core-ktx" version="2.8.1"> + <artifact name="lifecycle-livedata-core-ktx-2.8.1.aar"> + <sha256 value="875f86c77f83fc7581cb6e402da4ae3ab00a4daca7ba5ad5c64d46dd74016909" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-livedata-core-ktx-2.8.1.module"> + <sha256 value="e82b57a29ad49639557e59ae14833a046b664b6cd4c176f16aa7a7ff2180e027" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-livedata-core-ktx" version="2.8.2"> + <artifact name="lifecycle-livedata-core-ktx-2.8.2.aar"> + <sha256 value="31617dc7d89886b3ac6e93e4a781fe3c1f9b0b29050d4fc87c89ffd5d6cc821e" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-livedata-core-ktx-2.8.2.module"> + <sha256 value="58a4bc3ca72ec82edb7f41e3dff70bfde2ac8fe6404a4af5849f848b724a4522" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-livedata-ktx" version="2.8.1"> + <artifact name="lifecycle-livedata-ktx-2.8.1.aar"> + <sha256 value="538e0e732dbfbf05886c9f91f0f7871fa566c518631544b208d9ba38a8824f20" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-livedata-ktx-2.8.1.module"> + <sha256 value="5b28da2e5032b579b792bc9a8698b4ae0eda8ab48f00fa77123ed0885d6642c8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-livedata-ktx" version="2.8.2"> + <artifact name="lifecycle-livedata-ktx-2.8.2.aar"> + <sha256 value="273e77a59829d88c1d38bb02dcb6f5417896235fe9b399281f821d9c45174732" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-livedata-ktx-2.8.2.module"> + <sha256 value="34ac12793e78ac85e26050a59664c521205e785ef310b37fac6b48599dc04121" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-process" version="2.4.1"> + <artifact name="lifecycle-process-2.4.1.module"> + <sha256 value="e3aae3ed04b4744ff31452e98fd299e3adfd28d3b5723661d9678b92fa09ceb4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-process" version="2.8.1"> + <artifact name="lifecycle-process-2.8.1.aar"> + <sha256 value="fa6325d0742b929c6c10dd5558c02ba07cf47d8f922c70169cc86c7e1a353977" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-process-2.8.1.module"> + <sha256 value="06d9fff7202f3bdcd1d85165238d35a1b8c538848f1cbd41b118ce4b3b8ddabe" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-process" version="2.8.2"> + <artifact name="lifecycle-process-2.8.2.aar"> + <sha256 value="613688bfb3eaac259cf429d804b674325bab5245b9562576e4511a765fec3b04" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-process-2.8.2.module"> + <sha256 value="7e76faf10ecb3fe8213b4c242f8fa9b80da7942c070b4758b7a7a4217c9a9453" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-runtime" version="2.6.1"> + <artifact name="lifecycle-runtime-2.6.1.module"> + <sha256 value="a4cbb01a42d07047bd8d870017c96a1b0b7b4673320e86b66317a13be2ec10c7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-runtime" version="2.8.1"> + <artifact name="lifecycle-runtime-2.8.1.aar"> + <sha256 value="d4ae4e8dc0ca6265b683d7b2333a102a3ee84ad97ccb9fbec4334f5cd0e3f54d" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-runtime-2.8.1.module"> + <sha256 value="d0701961c5fdae079428dedd343b2ffd48ae68da5513b9977b35c5428657620b" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-runtime-metadata-2.8.1.jar"> + <sha256 value="4ee784530e550754230395d4f4f56817c9fcbc08db4d645e77441f3dd82c56b5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-runtime" version="2.8.2"> + <artifact name="lifecycle-runtime-2.8.2.aar"> + <sha256 value="d4ae4e8dc0ca6265b683d7b2333a102a3ee84ad97ccb9fbec4334f5cd0e3f54d" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-runtime-2.8.2.module"> + <sha256 value="a4b8fc08f65e12449421766b83a15d41a4325f412348078d755c175e7102abc4" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-runtime-metadata-2.8.2.jar"> + <sha256 value="4ee784530e550754230395d4f4f56817c9fcbc08db4d645e77441f3dd82c56b5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-runtime-android" version="2.8.1"> + <artifact name="lifecycle-runtime-android-2.8.1.module"> + <sha256 value="bdb365a1573a5d4633b015448be6d6808d643f42d4b55fc54d8d035b34079f22" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-runtime-release.aar"> + <sha256 value="386b722ee6198ac7c1f3c6cb080839cc979436ca07c4be78259aff21e912b979" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-runtime-android" version="2.8.2"> + <artifact name="lifecycle-runtime-android-2.8.2.module"> + <sha256 value="ca87b1aa11ada45b66f7d8c345a5df89fd0a678412db9a1720d02b2a5ea1c77e" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-runtime-release.aar"> + <sha256 value="158f313c1a177a66b31c696997ea3c16ff1673a8ac95b844fa227ad742d204d5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-runtime-desktop" version="2.8.1"> + <artifact name="lifecycle-runtime-desktop-2.8.1-sources.jar"> + <sha256 value="6c0abf455450dd51021173fb8ef37e964d8d9256db132f82937ef55d5e726fba" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-runtime-desktop-2.8.1.module"> + <sha256 value="cc08ed61614f92914191fb835edc873474aef1f40e42b9f731417fc63bf20c1d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-runtime-desktop" version="2.8.2"> + <artifact name="lifecycle-runtime-desktop-2.8.2-sources.jar"> + <sha256 value="6c0abf455450dd51021173fb8ef37e964d8d9256db132f82937ef55d5e726fba" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-runtime-desktop-2.8.2.module"> + <sha256 value="cc1e0192679467ed695987646a4e645d5060803f2913a687de71c5bc1186971c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-runtime-ktx" version="2.8.1"> + <artifact name="lifecycle-runtime-ktx-2.8.1.module"> + <sha256 value="ba3c03415162e1e470165b1d1254852fb286cf232e68fec54c57c410ab0810f3" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-runtime-ktx-metadata-2.8.1.jar"> + <sha256 value="92e90c3bd6cc2f66a15e9597cc015cd3a2f8767a9bd29d7feadaea4d44b2fccf" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-runtime-ktx" version="2.8.2"> + <artifact name="lifecycle-runtime-ktx-2.8.2.module"> + <sha256 value="3c7f723b662d1433797c659f48ddcdb53f3b2073b9dc9fd748941b67038d8969" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-runtime-ktx-metadata-2.8.2.jar"> + <sha256 value="92e90c3bd6cc2f66a15e9597cc015cd3a2f8767a9bd29d7feadaea4d44b2fccf" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-runtime-ktx-android" version="2.8.1"> + <artifact name="lifecycle-runtime-ktx-android-2.8.1.module"> + <sha256 value="68b84f501ecca9c4bb4641a54674a90be85a6643aad9cd9ad56a0ac87909e00c" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-runtime-ktx-release.aar"> + <sha256 value="6dabd1976b787a472cd8a5c0ebc8d7702cc9b9599137392111d2b42d4c58924b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-runtime-ktx-android" version="2.8.2"> + <artifact name="lifecycle-runtime-ktx-android-2.8.2.module"> + <sha256 value="a8878864db938c7ffe778d9296e7a7b5066954c2321a93ce33de37974098e120" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-runtime-ktx-release.aar"> + <sha256 value="cfcfaeba10fce09a5d8a1451f8d894edc60ac00552ea956ad7f561d5848ba868" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-service" version="2.8.1"> + <artifact name="lifecycle-service-2.8.1.aar"> + <sha256 value="94e1e1ffa817ffac23bb857b51981c92fbb97f8cc2c09f795b30f5e042308417" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-service-2.8.1.module"> + <sha256 value="c2ea282d707bf148949ba505c607e285f136b730047b5cb852a998eb4317861a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-service" version="2.8.2"> + <artifact name="lifecycle-service-2.8.2.aar"> + <sha256 value="aa83b5973b845ee5350c0f4358ffb137be25b71fa9f5c2cc51198fb9eb0e5201" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-service-2.8.2.module"> + <sha256 value="e7d7985bcef0c6738de2f2ba6968c06d8ebdfc06e0298fc1e2c5b3df3a0bd7c2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-viewmodel" version="2.6.1"> + <artifact name="lifecycle-viewmodel-2.6.1.module"> + <sha256 value="2b406faea5c12f2b8df4b7a60931f846648f2e1f4d78361e198d1184f19a4797" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-viewmodel" version="2.8.1"> + <artifact name="lifecycle-viewmodel-2.8.1.aar"> + <sha256 value="43d28a6c6da9c1cb72bfb5cc1b511a7937b2db8b79d52f37d0b7f14c79b090dd" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-2.8.1.module"> + <sha256 value="41bc1fd7d4f6884105fa96a5b372007c002e042c39f167e3903d3520f39a0c31" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-metadata-2.8.1.jar"> + <sha256 value="dd82d174a6da88070ebbe6fc842808e797424dd8605ff1d87f8d95b5bb992c08" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-viewmodel" version="2.8.2"> + <artifact name="lifecycle-viewmodel-2.8.2.aar"> + <sha256 value="43d28a6c6da9c1cb72bfb5cc1b511a7937b2db8b79d52f37d0b7f14c79b090dd" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-2.8.2.module"> + <sha256 value="72985daed5ac9f371b7328787d853bac6a95792461c5fae6243fa61e3d3f5608" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-metadata-2.8.2.jar"> + <sha256 value="539281344ce8a81d33570be300d86bb872c7b719a6dd2ce494eacba438fb53ce" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-viewmodel-android" version="2.8.1"> + <artifact name="lifecycle-viewmodel-android-2.8.1.module"> + <sha256 value="6731dc9056a3a533c10e42560e72e8748f2fca7a93092aeb0a11b4254e93c5ee" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-release.aar"> + <sha256 value="c312cc454cd006e69b8f80a990c871fef60098a748bf0dcfadf079aa7d526d4f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-viewmodel-android" version="2.8.2"> + <artifact name="lifecycle-viewmodel-android-2.8.2.module"> + <sha256 value="6ba5e376f07ef5328e884814c98a979bf3870b0aac4b39de7d981599abfdd0b9" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-release.aar"> + <sha256 value="e2f55a6e22af3f44a65469ef459e855fec7a77a454a1334315a1935b18ecda64" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-viewmodel-desktop" version="2.8.1"> + <artifact name="lifecycle-viewmodel-desktop-2.8.1-sources.jar"> + <sha256 value="0d8a81b0037fcd6beaea433263dc979ddd5d43f0c04f817a27e79c14fe0e7e81" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-desktop-2.8.1.module"> + <sha256 value="3a5568017c8170526a08874202081b00b7919194d534acb48f1630d1e900b14f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-viewmodel-desktop" version="2.8.2"> + <artifact name="lifecycle-viewmodel-desktop-2.8.2-sources.jar"> + <sha256 value="84f8224b9a8aea3c4a7dbec363e4dd59383c0b30f6f2d46d98053932cb8f002f" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-desktop-2.8.2.module"> + <sha256 value="67c8b7e0d90e19aa11309667f38a1aec57fb87c8d0291b7d4772b4f01ff6e6e3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-viewmodel-ktx" version="2.8.1"> + <artifact name="lifecycle-viewmodel-ktx-2.8.1-sources.jar"> + <sha256 value="c6deada2fac53b8ea6523dbda77597b128006674616f140f04df23264c6d1aa3" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-ktx-2.8.1.aar"> + <sha256 value="601d1e15c772a72d82aa35bf2654e44472851204d844f42856d6ff8c07c4e608" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-ktx-2.8.1.module"> + <sha256 value="48efc6de5e504a183abc41011fade076b0b21812fcb315cbb2fa4874634ab3a4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-viewmodel-ktx" version="2.8.2"> + <artifact name="lifecycle-viewmodel-ktx-2.8.2-sources.jar"> + <sha256 value="c6deada2fac53b8ea6523dbda77597b128006674616f140f04df23264c6d1aa3" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-ktx-2.8.2.aar"> + <sha256 value="b29dbe88d03faef613125339e8767e4e851b60789ddfd2fd141190d0b815a324" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-ktx-2.8.2.module"> + <sha256 value="3831834bb6851170557081e4a26a01591d3035af52a9b948ab14763584a0cc7a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-viewmodel-savedstate" version="2.8.1"> + <artifact name="lifecycle-viewmodel-savedstate-2.8.1-sources.jar"> + <sha256 value="e2a94fd97d7ecc72b7716dc98995fab44be80caf426d9fdb407b2109d4f853b1" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-savedstate-2.8.1.aar"> + <sha256 value="9dd54a05fabc8029fbfbbc4a6b74f946d70523e84ee246f7098669423e426708" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-savedstate-2.8.1.module"> + <sha256 value="4aebd9a7241c65dbe62e8fb0af2b25057e2f1352b188d2dc4bf76707eb291a9f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.lifecycle" name="lifecycle-viewmodel-savedstate" version="2.8.2"> + <artifact name="lifecycle-viewmodel-savedstate-2.8.2-sources.jar"> + <sha256 value="e2a94fd97d7ecc72b7716dc98995fab44be80caf426d9fdb407b2109d4f853b1" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-savedstate-2.8.2.aar"> + <sha256 value="2dc62627f265fa750d440c0d8589443faf04f77dce8a7efce3b390c52b0fe8dc" origin="Generated by Gradle"/> + </artifact> + <artifact name="lifecycle-viewmodel-savedstate-2.8.2.module"> + <sha256 value="d8ea7b73e8c300339417d1fff1e9af396c819f8bd24ad189d455158e0ce78783" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.loader" name="loader" version="1.0.0"> + <artifact name="loader-1.0.0.aar"> + <sha256 value="11f735cb3b55c458d470bed9e25254375b518b4b1bad6926783a7026db0f5025" origin="Generated by Gradle"/> + </artifact> + <artifact name="loader-1.0.0.pom"> + <sha256 value="c978d550808b47434aa49a63164110a50b55b0bcc6160a93a2e37d5110df8c5e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.localbroadcastmanager" name="localbroadcastmanager" version="1.0.0"> + <artifact name="localbroadcastmanager-1.0.0.aar"> + <sha256 value="e71c328ceef5c4a7d76f2d86df1b65d65fe2acf868b1a4efd84a3f34336186d8" origin="Generated by Gradle"/> + </artifact> + <artifact name="localbroadcastmanager-1.0.0.pom"> + <sha256 value="a000041f5a1f79283c5175e1bb60cf3683780f401c6a9d34fbe9751253fa6ff9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.media" name="media" version="1.7.0"> + <artifact name="media-1.7.0.aar"> + <sha256 value="81a199ee87c6d3d59fb35f7dbec71b3f1d507128611099b6ac3a4b1c5a0bf1f9" origin="Generated by Gradle"/> + </artifact> + <artifact name="media-1.7.0.module"> + <sha256 value="996f646284a2d981899cdac3282249e0e1f20ab82838a0875dc66bd264d24961" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.media3" name="media3-common" version="1.3.1"> + <artifact name="media3-common-1.3.1.aar"> + <sha256 value="40cf8c6c07f75afd0b213d694fd7ac478c726a94e64fa7ac1833c17752f3e6f4" origin="Generated by Gradle"/> + </artifact> + <artifact name="media3-common-1.3.1.module"> + <sha256 value="e03c4fa9d04188ffd7ca74ab2f8a2a0560afd24e151914c0af112c26d0e231af" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.media3" name="media3-container" version="1.3.1"> + <artifact name="media3-container-1.3.1.aar"> + <sha256 value="c6103c11666e03ceabeb6454b5bf0b9b51fdc93647f84dd02ee869a9c086269e" origin="Generated by Gradle"/> + </artifact> + <artifact name="media3-container-1.3.1.module"> + <sha256 value="1d3b35b97c19501ba3e0c0001511bef9526fc18d52f241471578922c99ee7f95" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.media3" name="media3-database" version="1.3.1"> + <artifact name="media3-database-1.3.1.aar"> + <sha256 value="8386d2c445d194268d4af2d63abb31b028da5f742fc38c6365bc2c40ab641383" origin="Generated by Gradle"/> + </artifact> + <artifact name="media3-database-1.3.1.module"> + <sha256 value="51df60a97ed7dcd7d35ecb174659bc0a9ee13ed9c15f239c888be28183f1e874" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.media3" name="media3-datasource" version="1.3.1"> + <artifact name="media3-datasource-1.3.1.aar"> + <sha256 value="ad94437e3c3dc5b9fb4a5c2dc64aa912a91125519742ec2fc3ae9ebfacc62381" origin="Generated by Gradle"/> + </artifact> + <artifact name="media3-datasource-1.3.1.module"> + <sha256 value="5c33611613f1f65d0e05ca01fb989f3af7be992cd8daef61ef528b5e6243d75f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.media3" name="media3-datasource-okhttp" version="1.3.1"> + <artifact name="media3-datasource-okhttp-1.3.1-sources.jar"> + <sha256 value="35c112f4e1c6c02d13e4f714c14971fb59c236cad507ff7f4f5a879c993144f7" origin="Generated by Gradle"/> + </artifact> + <artifact name="media3-datasource-okhttp-1.3.1.aar"> + <sha256 value="f4a578fd98dffdc2b4578421378ee4a61700ee250721595beb549ebbdb530d8f" origin="Generated by Gradle"/> + </artifact> + <artifact name="media3-datasource-okhttp-1.3.1.module"> + <sha256 value="5f566d63cc73d2f6d25d353339440e553e7a62b957c58efa5a8edecb48d382f6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.media3" name="media3-decoder" version="1.3.1"> + <artifact name="media3-decoder-1.3.1.aar"> + <sha256 value="c4799cd411061a654ad9c73fbc3d1a95945fce40f1bbceaf219e17b8a56fa1c1" origin="Generated by Gradle"/> + </artifact> + <artifact name="media3-decoder-1.3.1.module"> + <sha256 value="a9e432a7cd0722dfbf1f9d3dcf180103bf518826de12300825a391dc3c086442" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.media3" name="media3-exoplayer" version="1.3.1"> + <artifact name="media3-exoplayer-1.3.1-sources.jar"> + <sha256 value="c30282058a61aa6934ca6b2eaeb47383e233622fbda98977a8893c914f2d5907" origin="Generated by Gradle"/> + </artifact> + <artifact name="media3-exoplayer-1.3.1.aar"> + <sha256 value="00c4be0107fcb95ce62d5f255e0e07b169f25bfcda820054888e26a89bf0647f" origin="Generated by Gradle"/> + </artifact> + <artifact name="media3-exoplayer-1.3.1.module"> + <sha256 value="048ba3612f9a1c88de968b27e345faaf831c71d77c7dcc1abc840cc9ac6fda76" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.media3" name="media3-extractor" version="1.3.1"> + <artifact name="media3-extractor-1.3.1.aar"> + <sha256 value="54400e411960da947d85567bbce3e752fade6bcb7d075c122a4e2cd05379dc62" origin="Generated by Gradle"/> + </artifact> + <artifact name="media3-extractor-1.3.1.module"> + <sha256 value="1bc5f81099da08e99871eca3f2f2266806ef84a32499aeceb1dd6f8c80232329" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.media3" name="media3-ui" version="1.3.1"> + <artifact name="media3-ui-1.3.1-sources.jar"> + <sha256 value="c3d8d9a0c76996f07e9978e9e1176e344783b93ab3967d9c8b23ea414171a747" origin="Generated by Gradle"/> + </artifact> + <artifact name="media3-ui-1.3.1.aar"> + <sha256 value="a8d206f395838a06b82b0789e7200868dabb26b9c7207e98cc3b3196041c1c9d" origin="Generated by Gradle"/> + </artifact> + <artifact name="media3-ui-1.3.1.module"> + <sha256 value="043282ebb04a9d29ce8777bebb4e3ff3b59b257f343348ee9a54b6456688eb0f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.multidex" name="multidex" version="2.0.1"> + <artifact name="multidex-2.0.1.aar"> + <sha256 value="42dd32ff9f97f85771b82a20003a8d70f68ab7b4ba328964312ce0732693db09" origin="Generated by Gradle"/> + </artifact> + <artifact name="multidex-2.0.1.pom"> + <sha256 value="0f10b63cfd5292d16678077bdb78363447b832801936c401fc26e68293f2106f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.paging" name="paging-common" version="3.3.0"> + <artifact name="paging-common-3.3.0.module"> + <sha256 value="43c88ded7e80da969b5546af45cb6d6b8cf4332c8c1bb3a2c203244a4f2c5c94" origin="Generated by Gradle"/> + </artifact> + <artifact name="paging-common-metadata-3.3.0.jar"> + <sha256 value="3a00bde031ca057829a9e5c1722322a7660379033498f2a1ec837f33506f7e2b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.paging" name="paging-common-android" version="3.3.0"> + <artifact name="paging-common-android-3.3.0.module"> + <sha256 value="4a8ade98fe4a5bebc6ae4f54ebae9bc744d27e4b010893e651821a375aa6014d" origin="Generated by Gradle"/> + </artifact> + <artifact name="paging-common-release.aar"> + <sha256 value="7c536689141a77b2c1611b549380acbe1a7b8a9571f3335a215f29ccf5e9b8ef" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.paging" name="paging-common-ktx" version="3.3.0"> + <artifact name="paging-common-ktx-3.3.0.jar"> + <sha256 value="c6deada2fac53b8ea6523dbda77597b128006674616f140f04df23264c6d1aa3" origin="Generated by Gradle"/> + </artifact> + <artifact name="paging-common-ktx-3.3.0.module"> + <sha256 value="55fce6187b33470c5500f2d2a943e467e65636e176f1dccebca04757ebd01358" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.paging" name="paging-runtime" version="3.3.0"> + <artifact name="paging-runtime-3.3.0.aar"> + <sha256 value="1f7d21f225e7a549224f54f23900d90e9b7e0594bf503a45b531b70f5f2dd257" origin="Generated by Gradle"/> + </artifact> + <artifact name="paging-runtime-3.3.0.module"> + <sha256 value="2a6df791d38dce5caeabb93c179e93e0f01f7e6358bb10efe390107d4192a786" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.paging" name="paging-runtime-ktx" version="3.3.0"> + <artifact name="paging-runtime-ktx-3.3.0-sources.jar"> + <sha256 value="c6deada2fac53b8ea6523dbda77597b128006674616f140f04df23264c6d1aa3" origin="Generated by Gradle"/> + </artifact> + <artifact name="paging-runtime-ktx-3.3.0.aar"> + <sha256 value="1c6fd2223f829478305e00a2edd9355966173bd2796e80914edb8ac7a21c6877" origin="Generated by Gradle"/> + </artifact> + <artifact name="paging-runtime-ktx-3.3.0.module"> + <sha256 value="f8cdcbe7028da4e38612c170afdea27a563ad62e7346ae947078fd9e72847f3d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.preference" name="preference" version="1.2.1"> + <artifact name="preference-1.2.1.aar"> + <sha256 value="40ca8adfdb7effb61facb39bd9ca2e2f3a40d106743b0cd6dc9e21e8bedd4f85" origin="Generated by Gradle"/> + </artifact> + <artifact name="preference-1.2.1.module"> + <sha256 value="b9a4dc6d8d232878f717dfac00c77bf384bbb52c26e4064f7bcaa966c10a2629" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.preference" name="preference-ktx" version="1.2.1"> + <artifact name="preference-ktx-1.2.1-sources.jar"> + <sha256 value="225d80f9fb06bbd4137a448cdbd094147c9893848e5dbbd959498c2b2b2de700" origin="Generated by Gradle"/> + </artifact> + <artifact name="preference-ktx-1.2.1.aar"> + <sha256 value="872a5e3e07c0b8d21cb77769ab2f468b209ab6ec08231874401aecfea304a81c" origin="Generated by Gradle"/> + </artifact> + <artifact name="preference-ktx-1.2.1.module"> + <sha256 value="9532f2238b72155f8029f450c8cbe189b912a8dbffb32b6b9ffbdc2c7fd97b28" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.print" name="print" version="1.0.0"> + <artifact name="print-1.0.0.aar"> + <sha256 value="1d5c7f3135a1bba661fc373fd72e11eb0a4adbb3396787826dd8e4190d5d9edd" origin="Generated by Gradle"/> + </artifact> + <artifact name="print-1.0.0.pom"> + <sha256 value="62482c0594841bee24bb996abb6cb7b320a6a3b77dca9f0a0ba4fe3be5530aa7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.profileinstaller" name="profileinstaller" version="1.3.1"> + <artifact name="profileinstaller-1.3.1.aar"> + <sha256 value="d0e402ec31f24028a1dc7eb6a0a3f9d9635c1459392cd734396343b73d673948" origin="Generated by Gradle"/> + </artifact> + <artifact name="profileinstaller-1.3.1.module"> + <sha256 value="cc7eed0ed4b669de84b852f78797a50018a4f30002e1e38aaa668af22ca5b460" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.recyclerview" name="recyclerview" version="1.3.2"> + <artifact name="recyclerview-1.3.2-sources.jar"> + <sha256 value="62c1fc5b9158b6500d604fb345d49c9f6698832260140ec2dde553b5ab20d34e" origin="Generated by Gradle"/> + </artifact> + <artifact name="recyclerview-1.3.2.aar"> + <sha256 value="005cf51510493a24fa48baae1a455a5258515391351a71717dd33cba95211966" origin="Generated by Gradle"/> + </artifact> + <artifact name="recyclerview-1.3.2.module"> + <sha256 value="179fc4d2d022c0a8247e7612feab40b951bd8710f6b452e3ed6dc03a1e52769d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.resourceinspection" name="resourceinspection-annotation" version="1.0.1"> + <artifact name="resourceinspection-annotation-1.0.1.jar"> + <sha256 value="8cff870ec6fb31db48a52f4a792335b4bf8de07e03bd37823181526433ccd5cb" origin="Generated by Gradle"/> + </artifact> + <artifact name="resourceinspection-annotation-1.0.1.module"> + <sha256 value="352a11a8d8a4c1bd6cd2c2fefff9c94ca954d7b5202a0656959db95297f6a2b7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.room" name="room-common" version="2.6.1"> + <artifact name="room-common-2.6.1.jar"> + <sha256 value="99326d3eeca4a64671bc8802f1e45f4e6b348e8c7faba230c3854d316809ffcf" origin="Generated by Gradle"/> + </artifact> + <artifact name="room-common-2.6.1.module"> + <sha256 value="0ea07ef7143ab5262cc0e48e82f3badd6adf164b3701ab2ac0f6f099e8f63d3b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.room" name="room-compiler" version="2.6.1"> + <artifact name="room-compiler-2.6.1.jar"> + <sha256 value="2c103c27918e90b5b436d00f2024b057ab78af4325892e7bab78e7c6f8eed67c" origin="Generated by Gradle"/> + </artifact> + <artifact name="room-compiler-2.6.1.module"> + <sha256 value="32cee57602deb1320ef9f945a9ce3e91cfe652acba5844762297b9eaed12fff5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.room" name="room-compiler-processing" version="2.6.1"> + <artifact name="room-compiler-processing-2.6.1.jar"> + <sha256 value="6ce5f6c1aac61fc44595030cbf9e1c89cc29a29ea764472b4e4a49d272f0a7cd" origin="Generated by Gradle"/> + </artifact> + <artifact name="room-compiler-processing-2.6.1.module"> + <sha256 value="ff0671de4ba51085a23e97b3b58ecb498109e5ee0bd7fd4a72f2b7f9db765ca7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.room" name="room-ktx" version="2.6.1"> + <artifact name="room-ktx-2.6.1-sources.jar"> + <sha256 value="3118f596d5c3e70a2f8934db3d9655a71753f8d313dff794ab6b3ee1bf967547" origin="Generated by Gradle"/> + </artifact> + <artifact name="room-ktx-2.6.1.aar"> + <sha256 value="da4c0cba7efcafa29fbeab1db41984238f25c1a33612c1d60d63b995968d70ca" origin="Generated by Gradle"/> + </artifact> + <artifact name="room-ktx-2.6.1.module"> + <sha256 value="33ac3f42307328ac19d4fe52422337b1c1ae64db36e3933438312a48c3b3b631" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.room" name="room-migration" version="2.6.1"> + <artifact name="room-migration-2.6.1.jar"> + <sha256 value="0dd380522b6d55ab23902de873effc356e9a8e4fb8bd94dde963222d420a89b9" origin="Generated by Gradle"/> + </artifact> + <artifact name="room-migration-2.6.1.module"> + <sha256 value="3e85b971f8a5d94b3a81c440431691dd448256635009a2bd6925dd6dd1e3a42e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.room" name="room-paging" version="2.6.1"> + <artifact name="room-paging-2.6.1-sources.jar"> + <sha256 value="ae3b8198a108a5ee791412b649db773eaf60d88c0acc4a2a7a6fde1cf723e571" origin="Generated by Gradle"/> + </artifact> + <artifact name="room-paging-2.6.1.aar"> + <sha256 value="ab1b21ff01eedf513255e1feb9de1858b69faf2000ec0ed13570e42f7f250489" origin="Generated by Gradle"/> + </artifact> + <artifact name="room-paging-2.6.1.module"> + <sha256 value="cb3eee2cdc8e27ae986f975e65cc5410b50166cede9e21e91132ab9376dd072b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.room" name="room-runtime" version="2.5.0"> + <artifact name="room-runtime-2.5.0.module"> + <sha256 value="e321db49bf5e8425effc158472b958c831f4c6974501382ff8c98f89d4ebad4d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.room" name="room-runtime" version="2.6.1"> + <artifact name="room-runtime-2.6.1.aar"> + <sha256 value="69624fd7add6ce5bfcc12362cd427341d2910e277ed5a6fcc46132a4899114d0" origin="Generated by Gradle"/> + </artifact> + <artifact name="room-runtime-2.6.1.module"> + <sha256 value="828e4bead1b8d42ff17c41e74541133dc61a959b968f3c2a2221c24b3dac6c3b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.room" name="room-testing" version="2.6.1"> + <artifact name="room-testing-2.6.1-sources.jar"> + <sha256 value="e9c6087f3564e7eb41c21c15d00a5a2df6530203486cc672656638e215ff04ca" origin="Generated by Gradle"/> + </artifact> + <artifact name="room-testing-2.6.1.aar"> + <sha256 value="ea66d01428a8c8f26a0ee2259a96d7763dd0d3aef5321730790f8e9cfbe28360" origin="Generated by Gradle"/> + </artifact> + <artifact name="room-testing-2.6.1.module"> + <sha256 value="f87f1b1f90673636e17480929d25e417747cbef90253d7e63333596fa88611da" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.savedstate" name="savedstate" version="1.2.0"> + <artifact name="savedstate-1.2.0-sources.jar"> + <sha256 value="27056b2a9cddc7e5dc73700771f4e8dddbbcbab52b6cb46e33edc9588337e1cd" origin="Generated by Gradle"/> + </artifact> + <artifact name="savedstate-1.2.0.module"> + <sha256 value="4247c23308abcb5d0ff8cdaf7dfd583f3c2a1016f68d13f1a41c21600b6fafd7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.savedstate" name="savedstate" version="1.2.1"> + <artifact name="savedstate-1.2.1.aar"> + <sha256 value="21a7d4bcf6bdb94ad7b9283801529300b4fbb8808ca4f191e0cdce6fd8e4705a" origin="Generated by Gradle"/> + </artifact> + <artifact name="savedstate-1.2.1.module"> + <sha256 value="5bb656fc760d9e3996b535160cbb4106033c9f736e9089e6ef4eb0c669785066" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.savedstate" name="savedstate-ktx" version="1.2.1"> + <artifact name="savedstate-ktx-1.2.1.aar"> + <sha256 value="8553f87e7136c24ec5243560f48f1c32cba56daa77722f89589a5cafcb8f7894" origin="Generated by Gradle"/> + </artifact> + <artifact name="savedstate-ktx-1.2.1.module"> + <sha256 value="94359184b2ba51c0f498a2b9055d37b372231ef9bcc54a4972ac99f0303afff1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.sharetarget" name="sharetarget" version="1.2.0"> + <artifact name="sharetarget-1.2.0-sources.jar"> + <sha256 value="4893309311e53712084d106e803d0a8196b7c4b74a255ea69e12ae80f20cfe5c" origin="Generated by Gradle"/> + </artifact> + <artifact name="sharetarget-1.2.0.aar"> + <sha256 value="1d4e46d2c99b3ddf92163673cac212a4f0342ccf6827ff47606048b348ea78e3" origin="Generated by Gradle"/> + </artifact> + <artifact name="sharetarget-1.2.0.module"> + <sha256 value="5a35f1e23888d0eee77bfe481b000710aaac14edf123a8485edbd8fbcd18e059" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.slidingpanelayout" name="slidingpanelayout" version="1.2.0"> + <artifact name="slidingpanelayout-1.2.0.aar"> + <sha256 value="5f53339be2a4f90a9abea3571dd59e70a8a49e7f15dd82974a3898b4652e8714" origin="Generated by Gradle"/> + </artifact> + <artifact name="slidingpanelayout-1.2.0.module"> + <sha256 value="3531bf0081b78538589f7e840fc0f7c70a45c4d8a2d17fb2f0eaceee2a99b59b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.sqlite" name="sqlite" version="2.4.0"> + <artifact name="sqlite-2.4.0.aar"> + <sha256 value="bb7fa113112f7e4857496e222e3051d73a910add74bf40761e1bdae55b0216af" origin="Generated by Gradle"/> + </artifact> + <artifact name="sqlite-2.4.0.module"> + <sha256 value="b7b5f5fcb12a48f7fdeea82ef205721e3daf366fc77a843a77358def088f63fc" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.sqlite" name="sqlite-framework" version="2.3.0"> + <artifact name="sqlite-framework-2.3.0.module"> + <sha256 value="0b21ef275c000fe14bc73dfbf7b7699a289f3dc813c758ef37848269113e1cad" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.sqlite" name="sqlite-framework" version="2.4.0"> + <artifact name="sqlite-framework-2.4.0.aar"> + <sha256 value="ca6e503322b2e60374c2b36c95c50b16709d9388fe36e80fb23de1fbf7a6eb95" origin="Generated by Gradle"/> + </artifact> + <artifact name="sqlite-framework-2.4.0.module"> + <sha256 value="15681e94286ebf7121d940765a222b9a7ada2a32a49baa1ad416ef04b6f0f780" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.startup" name="startup-runtime" version="1.0.0"> + <artifact name="startup-runtime-1.0.0.module"> + <sha256 value="40effca0d6ee1fde32bc296897e54ebbcc4cf4aa29b0c531036cbd2a824a3c24" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.startup" name="startup-runtime" version="1.1.1"> + <artifact name="startup-runtime-1.1.1.aar"> + <sha256 value="e0a6329a371262fe4c450372b70fdaf33b769ef6917094723787cfce896b1dd3" origin="Generated by Gradle"/> + </artifact> + <artifact name="startup-runtime-1.1.1.module"> + <sha256 value="cfd96cf6450c6e2b697598924729ad9a0495c5a4fcf4ebee7e2f81b07e415865" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.swiperefreshlayout" name="swiperefreshlayout" version="1.1.0"> + <artifact name="swiperefreshlayout-1.1.0-sources.jar"> + <sha256 value="74e31bb677d99e9425f911f9f02341c7ed88eab57c15971fb17cb9cd1c9e826e" origin="Generated by Gradle"/> + </artifact> + <artifact name="swiperefreshlayout-1.1.0.aar"> + <sha256 value="2ce7906cd1dea05aec81975db22d54382359c05a21b2527ad848bc60f6b27293" origin="Generated by Gradle"/> + </artifact> + <artifact name="swiperefreshlayout-1.1.0.pom"> + <sha256 value="0b8c55ce6910155a283ba98a6d094be6e0ef5e70a7371cd9fe9f71bf8ffd24ad" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.test" name="annotation" version="1.0.1"> + <artifact name="annotation-1.0.1-sources.jar"> + <sha256 value="2880a29fd9120527ab45afceb611b62c62f5464a655a12801df63e78ed9d50ce" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotation-1.0.1.aar"> + <sha256 value="c0754928effe1968c3a9a7b55d1dfc7ceb1e1e7c9f3f09f98afd42431f712492" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotation-1.0.1.pom"> + <sha256 value="39e978b32726354a67ea6efe8535ab6579f4760fd579c9deaacb347b0bff0c3e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.test" name="core" version="1.5.0"> + <artifact name="core-1.5.0-sources.jar"> + <sha256 value="505e01edc532ac8259ad52264bc0f004f42043c446b47957438e673f82fd1944" origin="Generated by Gradle"/> + </artifact> + <artifact name="core-1.5.0.aar"> + <sha256 value="2c06715c0d0843cee2143ab8bb322bb3f34d5247630402fc8c1b6a0eafa15b9f" origin="Generated by Gradle"/> + </artifact> + <artifact name="core-1.5.0.pom"> + <sha256 value="18a6187c16b8d14b51b6a69e32de4a1416a0dc12cff3357d56fbe5feb70a15f2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.test" name="monitor" version="1.6.1"> + <artifact name="monitor-1.6.1-sources.jar"> + <sha256 value="50517fcf104a71f3d3c5d28802b2e82876f8e5a4b63457979d09b08f22481807" origin="Generated by Gradle"/> + </artifact> + <artifact name="monitor-1.6.1.aar"> + <sha256 value="2985ce8556989baf7c84342e7f687713c037a39a922e614d1a3ddf1ca3777079" origin="Generated by Gradle"/> + </artifact> + <artifact name="monitor-1.6.1.pom"> + <sha256 value="30c0dab3944d63721e2023b27ff35ef343130c9eb7b88f49f331b4e0f2ecfbce" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.test" name="runner" version="1.5.2"> + <artifact name="runner-1.5.2-sources.jar"> + <sha256 value="188f6e40732dda9451d70121a12a31f6411bb3ae598d58193e494ff27e111700" origin="Generated by Gradle"/> + </artifact> + <artifact name="runner-1.5.2.aar"> + <sha256 value="36cd6bc876daa1f183ccd11f9898e094c71f06960fde85a373422959613a44d6" origin="Generated by Gradle"/> + </artifact> + <artifact name="runner-1.5.2.pom"> + <sha256 value="505c188f187d91d76623b7898261ea0137d902aa1a4e143d88cc2c23742b4ac1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.test.espresso" name="espresso-core" version="3.5.1"> + <artifact name="espresso-core-3.5.1-sources.jar"> + <sha256 value="f12ec3d9ec4de40cfa901b1d397e4bb30caefe13e9029d44606ec39f2d9b6d9f" origin="Generated by Gradle"/> + </artifact> + <artifact name="espresso-core-3.5.1.aar"> + <sha256 value="34b0493f4e002f205d961e562add0c0c31bb0acc657e89d89d4b188ac13f242c" origin="Generated by Gradle"/> + </artifact> + <artifact name="espresso-core-3.5.1.pom"> + <sha256 value="f15bd19444eef421f74195cc7043343af2b091a0cdb05a02cb8fcaba8816b50e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.test.espresso" name="espresso-idling-resource" version="3.5.1"> + <artifact name="espresso-idling-resource-3.5.1-sources.jar"> + <sha256 value="20e8d17706649666730eb05ce2b148a4295465aec765e4576856a1ab3164dc25" origin="Generated by Gradle"/> + </artifact> + <artifact name="espresso-idling-resource-3.5.1.aar"> + <sha256 value="84fb8e2f5eda937771bee28582f5d2cfa61b0e9438d02041ca61b81e3dac3c87" origin="Generated by Gradle"/> + </artifact> + <artifact name="espresso-idling-resource-3.5.1.pom"> + <sha256 value="c9acad1f91f46e9ef59b000a3e995c9da9d6949dfa77ab8a2dadfb26f295cee1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.test.ext" name="junit" version="1.1.5"> + <artifact name="junit-1.1.5-sources.jar"> + <sha256 value="751febaf516e08ff72749e693b2f5351c97af8a1451c23dcf18b522cd1940e96" origin="Generated by Gradle"/> + </artifact> + <artifact name="junit-1.1.5.aar"> + <sha256 value="4307c0e60f5d701db9c59bcd9115af705113c36a9132fa3dbad58db1294e9bfd" origin="Generated by Gradle"/> + </artifact> + <artifact name="junit-1.1.5.pom"> + <sha256 value="4cff0df04cae25831e821ef2f9129245783460e98d0fd67d8f6824065a134c4e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.test.services" name="storage" version="1.4.2"> + <artifact name="storage-1.4.2-sources.jar"> + <sha256 value="160b0c0c957fa33046d93942a0937b7d08bc09ccb51003e83c1e94f65b64034a" origin="Generated by Gradle"/> + </artifact> + <artifact name="storage-1.4.2.aar"> + <sha256 value="b34861f0cd920cb1089f08c3f27e5865b7f920284cc45f4ed12ef8d6980dac48" origin="Generated by Gradle"/> + </artifact> + <artifact name="storage-1.4.2.pom"> + <sha256 value="9b6301b11212641fa2cea00e48a98d0bf6f9de9fa4eabfdb31828b9a97c3ad89" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.tracing" name="tracing" version="1.0.0"> + <artifact name="tracing-1.0.0-sources.jar"> + <sha256 value="a593c115d145df9cb9ae2226960da1d53ff8590eff1c285e7c9de2b6727d3c21" origin="Generated by Gradle"/> + </artifact> + <artifact name="tracing-1.0.0.aar"> + <sha256 value="07b8b6139665b884a162eccf97891ca50f7f56831233bf25168ae04f7b568612" origin="Generated by Gradle"/> + </artifact> + <artifact name="tracing-1.0.0.module"> + <sha256 value="fc8b21ebe5fa3a7c96ee098bcdcd00f077ebce73f243fa858e2b0671615f75d8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.transition" name="transition" version="1.5.0"> + <artifact name="transition-1.5.0.aar"> + <sha256 value="0aa66a0ea406d25a1091f96a3b753b4b12e44fdc43b91ec52c17831e9c31f54b" origin="Generated by Gradle"/> + </artifact> + <artifact name="transition-1.5.0.module"> + <sha256 value="ad433467f5ede87ad2b3c59f7fe063984cf0720cdacd10591f5d8f84e3a4e095" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.vectordrawable" name="vectordrawable" version="1.1.0"> + <artifact name="vectordrawable-1.1.0.aar"> + <sha256 value="46fd633ac01b49b7fcabc263bf098c5a8b9e9a69774d234edcca04fb02df8e26" origin="Generated by Gradle"/> + </artifact> + <artifact name="vectordrawable-1.1.0.pom"> + <sha256 value="5b0e2d5b2179e54804785cbc21ce5f473b5e1ddd55a57da482e94dcd39492bb2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.vectordrawable" name="vectordrawable-animated" version="1.1.0"> + <artifact name="vectordrawable-animated-1.1.0-sources.jar"> + <sha256 value="a3880c595965c55fd1bdee0554b99c66e9c28d3311b7ce6242189fc1f554fcf1" origin="Generated by Gradle"/> + </artifact> + <artifact name="vectordrawable-animated-1.1.0.aar"> + <sha256 value="76da2c502371d9c38054df5e2b248d00da87809ed058f3363eae87ce5e2403f8" origin="Generated by Gradle"/> + </artifact> + <artifact name="vectordrawable-animated-1.1.0.pom"> + <sha256 value="276a20116b705fb75b9003ee9496c56f6fd3b32375fb232472811eba60a040bd" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.versionedparcelable" name="versionedparcelable" version="1.1.0"> + <artifact name="versionedparcelable-1.1.0.aar"> + <sha256 value="9a1d77140ac222b7866b5054ee7d159bc1800987ed2d46dd6afdd145abb710c1" origin="Generated by Gradle"/> + </artifact> + <artifact name="versionedparcelable-1.1.0.pom"> + <sha256 value="c729c7be0cc06323bda829d460666e79dbd43b799a21089a44bd3b293dc253b5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.versionedparcelable" name="versionedparcelable" version="1.1.1"> + <artifact name="versionedparcelable-1.1.1.aar"> + <sha256 value="57e8d93260d18d5b9007c9eed3c64ad159de90c8609ebfc74a347cbd514535a4" origin="Generated by Gradle"/> + </artifact> + <artifact name="versionedparcelable-1.1.1.pom"> + <sha256 value="5f51e65873ca612de3838fa90d2ee95b8d040efd31b9c390a19bf94d615cdb2f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.viewpager" name="viewpager" version="1.0.0"> + <artifact name="viewpager-1.0.0.aar"> + <sha256 value="147af4e14a1984010d8f155e5e19d781f03c1d70dfed02a8e0d18428b8fc8682" origin="Generated by Gradle"/> + </artifact> + <artifact name="viewpager-1.0.0.pom"> + <sha256 value="1f72f836339d03c6eb013f65075e76ca87075a577578eb4f95f74a3a5d253128" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.viewpager2" name="viewpager2" version="1.1.0"> + <artifact name="viewpager2-1.1.0-sources.jar"> + <sha256 value="2dfd4e7bd4009a7d0ad691f0bae351ef3bd2fdcb5fe3270b49216e47fb6262b9" origin="Generated by Gradle"/> + </artifact> + <artifact name="viewpager2-1.1.0.aar"> + <sha256 value="1934b5eff05c46215896b75bac065a7b63b13e3f0c8deacc0a9d54b207d9f53d" origin="Generated by Gradle"/> + </artifact> + <artifact name="viewpager2-1.1.0.module"> + <sha256 value="f4f492a0e9a3082e7de77d3eb1664b31c967e7b768d7e7586fbc493d36291a26" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.window" name="window" version="1.0.0"> + <artifact name="window-1.0.0.aar"> + <sha256 value="3212985be4127373ca4d0ea7f8b81a250ae2105e924f7940105d067a0f9ac130" origin="Generated by Gradle"/> + </artifact> + <artifact name="window-1.0.0.module"> + <sha256 value="536773d2b2d65c26ce06b8c95e0fb415f1ad25d9b87330170f508689d3ad5ffb" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.work" name="work-runtime" version="2.9.0"> + <artifact name="work-runtime-2.9.0.aar"> + <sha256 value="8b85f38aa826d902e8a88d2af9dc73a5f43f6618720485f16e742692c5a18f6f" origin="Generated by Gradle"/> + </artifact> + <artifact name="work-runtime-2.9.0.module"> + <sha256 value="94ac1c0cb2fa9e145e000e009ec22de8abecb70a654fd1cf8e9e8a0b02a7fd53" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.work" name="work-runtime-ktx" version="2.9.0"> + <artifact name="work-runtime-ktx-2.9.0-sources.jar"> + <sha256 value="c6deada2fac53b8ea6523dbda77597b128006674616f140f04df23264c6d1aa3" origin="Generated by Gradle"/> + </artifact> + <artifact name="work-runtime-ktx-2.9.0.aar"> + <sha256 value="3c2f9a0a51826f1ba747751d899d9857def9976c09f8ea90066b364abcb24a19" origin="Generated by Gradle"/> + </artifact> + <artifact name="work-runtime-ktx-2.9.0.module"> + <sha256 value="49517d6afaa941627ecca80ae12bdd48d215a9c0222c8967babac7a9237e522a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="androidx.work" name="work-testing" version="2.9.0"> + <artifact name="work-testing-2.9.0-sources.jar"> + <sha256 value="7ed3ec16b4203d209cec89f6e467d28359eba77de5cd9165b6430eea0a64eebf" origin="Generated by Gradle"/> + </artifact> + <artifact name="work-testing-2.9.0.aar"> + <sha256 value="b5dbe5a96430c0f6ba225f1b9d66ee3128408f73714b9919524510c9bf9f4efd" origin="Generated by Gradle"/> + </artifact> + <artifact name="work-testing-2.9.0.module"> + <sha256 value="b144f2d4de2f2ebe906ebd2cc5a208a97d06e1e84000bc209c947f280d80e7f4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="app.cash.turbine" name="turbine" version="1.1.0"> + <artifact name="Turbine-metadata.jar"> + <sha256 value="ef32d46a2e2e04b748f8e105ebb96508ba2f7c0cf28f689a3160774f33bee22d" origin="Generated by Gradle"/> + </artifact> + <artifact name="turbine-1.1.0.module"> + <sha256 value="74c94344524b0ab306f2770f1daec40a273483652a492eae3b1bbf71dde4ce47" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="app.cash.turbine" name="turbine-jvm" version="1.1.0"> + <artifact name="Turbine-jvm-sources.jar"> + <sha256 value="7fc495f7e1c208fd55d9945f1b5cda0db98b2a83b133ae0bf3a43679745b9282" origin="Generated by Gradle"/> + </artifact> + <artifact name="Turbine-jvm.jar"> + <sha256 value="9b779f2390bc0e56c5f86cbf3552b0b41b53e4c2c2322b53ba1f796054026356" origin="Generated by Gradle"/> + </artifact> + <artifact name="turbine-jvm-1.1.0.module"> + <sha256 value="ba7cc8f9f368b437ccb16a5af704167696cd4f00ea8e67ca7243acaff6c99cd3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="at.connyduck" name="networkresult-calladapter" version="1.1.0"> + <artifact name="networkresult-calladapter-1.1.0.jar"> + <sha256 value="61ab47bccfece072ad6f10ca5c775cfc7cb0625f747941e3bd92aaf8cd36f8e1" origin="Generated by Gradle"/> + </artifact> + <artifact name="networkresult-calladapter-1.1.0.module"> + <sha256 value="a71fdc421ba8c13bd1bbf7b9e6a4f3c7855c34c5881d9af3fd291101d6bdb727" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="at.connyduck.sparkbutton" name="sparkbutton" version="4.2.0"> + <artifact name="sparkbutton-4.2.0-sources.jar"> + <sha256 value="449194742914537cb1907659e92d1d194a8c23bdfbf4bd82679b99774b3fefb3" origin="Generated by Gradle"/> + </artifact> + <artifact name="sparkbutton-4.2.0.aar"> + <sha256 value="dfbf78d1ee8f633c59d3f928035729d3575ee9345317207848f1ad099e792433" origin="Generated by Gradle"/> + </artifact> + <artifact name="sparkbutton-4.2.0.module"> + <sha256 value="9dc4059c1d6233cc9c78a20b54a44b0a2b801da5b7fcefe2b7a5b695055172b2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="ch.qos.logback" name="logback-classic" version="1.3.5"> + <artifact name="logback-classic-1.3.5.jar"> + <sha256 value="9d68b9daf2fbb98a09b0445e0c64ac309d9d0e156b68510b63791397c97e32e4" origin="Generated by Gradle"/> + </artifact> + <artifact name="logback-classic-1.3.5.pom"> + <sha256 value="ec75ac871a3d1361dbb0f8db6d25035bab89a5ea973f184ab1efa91ceee846c5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="ch.qos.logback" name="logback-core" version="1.3.5"> + <artifact name="logback-core-1.3.5.jar"> + <sha256 value="b1f0ec393f2f5bbaf1decb61624de18386634c3197e9eed69d1c1234ddd756f3" origin="Generated by Gradle"/> + </artifact> + <artifact name="logback-core-1.3.5.pom"> + <sha256 value="1e0746253f03f7de6f5bbd154336aa4d758694210f69cfff6e8eb5f8efc35528" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="ch.qos.logback" name="logback-parent" version="1.3.5"> + <artifact name="logback-parent-1.3.5.pom"> + <sha256 value="11800a2d0a4bcd442964da561c11a11f16e44b89fea6b10a891a109228079f5b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.almworks.sqlite4java" name="sqlite4java" version="1.0.392"> + <artifact name="sqlite4java-1.0.392.jar"> + <sha256 value="243a64470fda0e86a6fddeb0af4c7aa9426ce84e68cbfe18d75ee5da4b7e0b92" origin="Generated by Gradle"/> + </artifact> + <artifact name="sqlite4java-1.0.392.pom"> + <sha256 value="139552c586a57bf6d98f87d6b7e23fef4db53cf74097be962f7868e3606c79d2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android" name="signflinger" version="8.4.1"> + <artifact name="signflinger-8.4.1.jar"> + <sha256 value="c1dca2c683634ee1a294298f9c7179578af6a86e080bdc40f961915bc5c8142f" origin="Generated by Gradle"/> + </artifact> + <artifact name="signflinger-8.4.1.pom"> + <sha256 value="2f90457ef7453e9a2bc0e1797c1e049db3eab2c1f4b0b6952677c7f093c76433" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android" name="signflinger" version="8.4.2"> + <artifact name="signflinger-8.4.2.jar"> + <sha256 value="c1dca2c683634ee1a294298f9c7179578af6a86e080bdc40f961915bc5c8142f" origin="Generated by Gradle"/> + </artifact> + <artifact name="signflinger-8.4.2.pom"> + <sha256 value="3b7e471c7a791b7d5a0bbe632ae029ae5d79835861fed14f9d2fec4c5970c5a2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android" name="signflinger" version="8.5.0"> + <artifact name="signflinger-8.5.0.jar"> + <sha256 value="c1dca2c683634ee1a294298f9c7179578af6a86e080bdc40f961915bc5c8142f" origin="Generated by Gradle"/> + </artifact> + <artifact name="signflinger-8.5.0.pom"> + <sha256 value="a5a43e06e4ab2bdf78fdc825e19a94f08937eb2941c2ab66867fef76961918d5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android" name="zipflinger" version="8.4.1"> + <artifact name="zipflinger-8.4.1.jar"> + <sha256 value="81dd485618a509a3235929b9eb13091d884452661de6ce5a45cc38b1c555421c" origin="Generated by Gradle"/> + </artifact> + <artifact name="zipflinger-8.4.1.pom"> + <sha256 value="1f52e3f76df70c4d40c0af93297ae37949aac8f8aab534edae0ab61f69b02660" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android" name="zipflinger" version="8.4.2"> + <artifact name="zipflinger-8.4.2.jar"> + <sha256 value="81dd485618a509a3235929b9eb13091d884452661de6ce5a45cc38b1c555421c" origin="Generated by Gradle"/> + </artifact> + <artifact name="zipflinger-8.4.2.pom"> + <sha256 value="25977ab325653fb5e3318b323dc697c331b83d015d5b209b8c463fee7861e11d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android" name="zipflinger" version="8.5.0"> + <artifact name="zipflinger-8.5.0.jar"> + <sha256 value="81dd485618a509a3235929b9eb13091d884452661de6ce5a45cc38b1c555421c" origin="Generated by Gradle"/> + </artifact> + <artifact name="zipflinger-8.5.0.pom"> + <sha256 value="be4acb90c8bc1dab0981469af66985878eba2abcdc748339e5405a23ba9ab021" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.application" name="com.android.application.gradle.plugin" version="8.4.1"> + <artifact name="com.android.application.gradle.plugin-8.4.1.pom"> + <sha256 value="7b31439a481ae18152bba5b3d416099c215cd6cbbd4ee48cac24d6ed53b2d4a5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.application" name="com.android.application.gradle.plugin" version="8.4.2"> + <artifact name="com.android.application.gradle.plugin-8.4.2.pom"> + <sha256 value="1f30c798b3a279a9b8703bc37d49d4fe6c329b743b99b8e0c74e7798f30f91e4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.application" name="com.android.application.gradle.plugin" version="8.5.0"> + <artifact name="com.android.application.gradle.plugin-8.5.0.pom"> + <sha256 value="3c76fdfc6feb259fa5a97f00f179d83ab8af62462730429a39742ec53bdff2d9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.databinding" name="baseLibrary" version="8.4.1"> + <artifact name="baseLibrary-8.4.1.jar"> + <sha256 value="794113709dab21b06c262b3795e73cb708fbacae61715f34361e1af6237a1870" origin="Generated by Gradle"/> + </artifact> + <artifact name="baseLibrary-8.4.1.pom"> + <sha256 value="8d6d83207f9ab92f4d0a62555c1562e6a70995c3ccb56ecd1417c70927fd7117" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.databinding" name="baseLibrary" version="8.4.2"> + <artifact name="baseLibrary-8.4.2.jar"> + <sha256 value="794113709dab21b06c262b3795e73cb708fbacae61715f34361e1af6237a1870" origin="Generated by Gradle"/> + </artifact> + <artifact name="baseLibrary-8.4.2.pom"> + <sha256 value="d169bf83ecc4cbb0e4dd621fc5f2267abdf57ced812981cd4205d0b49fa38c6a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.databinding" name="baseLibrary" version="8.5.0"> + <artifact name="baseLibrary-8.5.0.jar"> + <sha256 value="794113709dab21b06c262b3795e73cb708fbacae61715f34361e1af6237a1870" origin="Generated by Gradle"/> + </artifact> + <artifact name="baseLibrary-8.5.0.pom"> + <sha256 value="f609e4b4743e8edec514bc5d84d78abc324478817eb3dd529e1e731da4160019" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="annotations" version="31.4.1"> + <artifact name="annotations-31.4.1.jar"> + <sha256 value="25e8825547de7146d08fa18f04ef934ee1e6aec52e4c97ffa510a8f0f44b07a0" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotations-31.4.1.pom"> + <sha256 value="0ecb544939cb910df229096e8a6bbe9f05912e846ea36c885c39432f802bbdbc" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="annotations" version="31.4.2"> + <artifact name="annotations-31.4.2.jar"> + <sha256 value="25e8825547de7146d08fa18f04ef934ee1e6aec52e4c97ffa510a8f0f44b07a0" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotations-31.4.2.pom"> + <sha256 value="85200d12613062f2bd913affeacb4a8bb1061762db3f6f51041eca8424f7a80e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="annotations" version="31.5.0"> + <artifact name="annotations-31.5.0.jar"> + <sha256 value="b25995fa7b220d35fbb8e325df071f8291691bfbaff9734c98e38f45ec2a73ee" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotations-31.5.0.pom"> + <sha256 value="372cd3363009e54aae8b96fe2e7681234913ae966c696c0e5b065d3f628af342" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="common" version="31.4.1"> + <artifact name="common-31.4.1.jar"> + <sha256 value="37a718ab8453e4152aa75bb898aaad3d80473a9a69663f0c3d3cdb63b130cc3e" origin="Generated by Gradle"/> + </artifact> + <artifact name="common-31.4.1.pom"> + <sha256 value="2b5c7803cdb2712b908efb74c35aa1a5d31631beaae61f5a1e4f07325d2f02e9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="common" version="31.4.2"> + <artifact name="common-31.4.2.jar"> + <sha256 value="ad5a602603bdc34688c71dd14de85ba72851cb393bcc6bcfc6d0ff46bd77a40a" origin="Generated by Gradle"/> + </artifact> + <artifact name="common-31.4.2.pom"> + <sha256 value="8011158cabb3ee615926342ec60c8f02e60193c7830110c81845af38eb06fe56" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="common" version="31.5.0"> + <artifact name="common-31.5.0.jar"> + <sha256 value="a9247467a3a4db53d8e839be6c6ff114ba8f364a1034827d11391c5385cb3ddd" origin="Generated by Gradle"/> + </artifact> + <artifact name="common-31.5.0.pom"> + <sha256 value="68c1120187945862450b40d1c6ce1a2cc1454713b40a78268e887fb0aed9eb64" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="dvlib" version="31.4.1"> + <artifact name="dvlib-31.4.1.jar"> + <sha256 value="bf962bea906e1dc8a3273b8129999eaea3bb16fcb2b9deea1450dc6fb2ef9073" origin="Generated by Gradle"/> + </artifact> + <artifact name="dvlib-31.4.1.pom"> + <sha256 value="551675e33480c7df6b28c87c9655ab024cadd3dadc9311659ca4b1b8987ddb97" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="dvlib" version="31.4.2"> + <artifact name="dvlib-31.4.2.jar"> + <sha256 value="bf962bea906e1dc8a3273b8129999eaea3bb16fcb2b9deea1450dc6fb2ef9073" origin="Generated by Gradle"/> + </artifact> + <artifact name="dvlib-31.4.2.pom"> + <sha256 value="340d38124a0dac119240f1fd9f08451afe8224e41f23fe619bc4568cb69d5491" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="dvlib" version="31.5.0"> + <artifact name="dvlib-31.5.0.jar"> + <sha256 value="5ccc490258202850630c59eecb9d29a6c849bb17f10f4ca9017db649f72e481f" origin="Generated by Gradle"/> + </artifact> + <artifact name="dvlib-31.5.0.pom"> + <sha256 value="8a4c9935c83097bff3af25c97b47039361840aab79fe2929e1f23f60abf68056" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="play-sdk-proto" version="31.4.1"> + <artifact name="play-sdk-proto-31.4.1.jar"> + <sha256 value="68840b9dc064724dbe12844e0c59e325ac4dfaa818bf3262635bd5254010a29c" origin="Generated by Gradle"/> + </artifact> + <artifact name="play-sdk-proto-31.4.1.pom"> + <sha256 value="184970856b7258e1956978203d14aa68f36d09fd4a1978bb9b11808463ed6c7e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="play-sdk-proto" version="31.4.2"> + <artifact name="play-sdk-proto-31.4.2.jar"> + <sha256 value="68840b9dc064724dbe12844e0c59e325ac4dfaa818bf3262635bd5254010a29c" origin="Generated by Gradle"/> + </artifact> + <artifact name="play-sdk-proto-31.4.2.pom"> + <sha256 value="d3759ae9bba0117cd0a09fb9dce965f7c637891e4c7ba8b4c5d05e651bbe7dc1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="play-sdk-proto" version="31.5.0"> + <artifact name="play-sdk-proto-31.5.0.jar"> + <sha256 value="eccafb032ee4b76514db676a45cd4c5b0bd5be9ea7494f8debdac765e89bb92e" origin="Generated by Gradle"/> + </artifact> + <artifact name="play-sdk-proto-31.5.0.pom"> + <sha256 value="631ae63935470a8ea93696e248c3a37fbe35bcf2e3e941b1207875fff194ecb5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="repository" version="31.4.1"> + <artifact name="repository-31.4.1.jar"> + <sha256 value="438597d95beb6d2c3bf17e012c4f46d533660b4f9f36c6181f455ec45cc21c68" origin="Generated by Gradle"/> + </artifact> + <artifact name="repository-31.4.1.pom"> + <sha256 value="db62b26da28d0e56f2c9aa8b48772b433afd3555cb7281bfc42376063e14d978" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="repository" version="31.4.2"> + <artifact name="repository-31.4.2.jar"> + <sha256 value="438597d95beb6d2c3bf17e012c4f46d533660b4f9f36c6181f455ec45cc21c68" origin="Generated by Gradle"/> + </artifact> + <artifact name="repository-31.4.2.pom"> + <sha256 value="282f74eb1f315cc63a29685865572585dd95aa479a3c53a85fb9beddcaf4dbdb" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="repository" version="31.5.0"> + <artifact name="repository-31.5.0.jar"> + <sha256 value="df2533e154b736a0a95e4804893aa59099552842bf9635cda3751be9e894a0e2" origin="Generated by Gradle"/> + </artifact> + <artifact name="repository-31.5.0.pom"> + <sha256 value="79363014583db7da814c01a6ffe89a83d75eced16ce5f09f2edd872dcfb0dfda" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="sdk-common" version="31.4.1"> + <artifact name="sdk-common-31.4.1.jar"> + <sha256 value="f38eba4bbfb6d541dc8c2be38c31e7389d0acdc354b234f258b6ee89e8ddce6d" origin="Generated by Gradle"/> + </artifact> + <artifact name="sdk-common-31.4.1.pom"> + <sha256 value="d1f7a3098bf5f1fa6f93add88c3972ec3f7781da4d7814b6cf7035a908623490" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="sdk-common" version="31.4.2"> + <artifact name="sdk-common-31.4.2.jar"> + <sha256 value="f38eba4bbfb6d541dc8c2be38c31e7389d0acdc354b234f258b6ee89e8ddce6d" origin="Generated by Gradle"/> + </artifact> + <artifact name="sdk-common-31.4.2.pom"> + <sha256 value="a3a152875f17de4343bce9eff5cdd2274020d4167e0de8d2f184fc3d9fe95ad0" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="sdk-common" version="31.5.0"> + <artifact name="sdk-common-31.5.0.jar"> + <sha256 value="ca66994d093167e389b213247f414e6f9a94584db78c2e53c94b84fb60b60b9a" origin="Generated by Gradle"/> + </artifact> + <artifact name="sdk-common-31.5.0.pom"> + <sha256 value="197630c2b9aa43bac4786a490bd4235e99c30a9bf92c5d216920ae0710d9d24d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="sdklib" version="31.4.1"> + <artifact name="sdklib-31.4.1.jar"> + <sha256 value="51b499a01f398d6b671911f9d85b3114fde5e462f57b0040c5f0fcab1bdbfc22" origin="Generated by Gradle"/> + </artifact> + <artifact name="sdklib-31.4.1.pom"> + <sha256 value="9d6bc666b2a3512d3c004e040f30501fba6c384384b784cc655a11dd9881a17d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="sdklib" version="31.4.2"> + <artifact name="sdklib-31.4.2.jar"> + <sha256 value="51b499a01f398d6b671911f9d85b3114fde5e462f57b0040c5f0fcab1bdbfc22" origin="Generated by Gradle"/> + </artifact> + <artifact name="sdklib-31.4.2.pom"> + <sha256 value="57d3df328b7f791a4434d606eeae5e388602429a56515d6f5f6ddb6bad52c49a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools" name="sdklib" version="31.5.0"> + <artifact name="sdklib-31.5.0.jar"> + <sha256 value="a87c7d95a6d9c329ddde828af3f213253140135c60a0bf025a9107b3c2123f20" origin="Generated by Gradle"/> + </artifact> + <artifact name="sdklib-31.5.0.pom"> + <sha256 value="4070cd53a287e164d8eb8be3b2d0caddbb8d0667b470b211a1d01c4a5d6ac178" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.analytics-library" name="crash" version="31.4.1"> + <artifact name="crash-31.4.1.jar"> + <sha256 value="2c5c26420967675c074bc7140831fe3bff5bbabc7fbbc08ba29fdc3eadcc9510" origin="Generated by Gradle"/> + </artifact> + <artifact name="crash-31.4.1.pom"> + <sha256 value="d2b9646234c18314a40042a81e4f749a3ed7cc3a4a24fc0ffbcbdf1284ecf8d7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.analytics-library" name="crash" version="31.4.2"> + <artifact name="crash-31.4.2.jar"> + <sha256 value="2c5c26420967675c074bc7140831fe3bff5bbabc7fbbc08ba29fdc3eadcc9510" origin="Generated by Gradle"/> + </artifact> + <artifact name="crash-31.4.2.pom"> + <sha256 value="d8543c8b78b807f510ceceda53610f726caaddd3d706d6a5521ec2416c3eaa18" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.analytics-library" name="crash" version="31.5.0"> + <artifact name="crash-31.5.0.jar"> + <sha256 value="2c5c26420967675c074bc7140831fe3bff5bbabc7fbbc08ba29fdc3eadcc9510" origin="Generated by Gradle"/> + </artifact> + <artifact name="crash-31.5.0.pom"> + <sha256 value="8a55aa79961c628ffcf25f88d7f76001dbedf7b7910370daf47b8c4ca752189f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.analytics-library" name="protos" version="31.4.1"> + <artifact name="protos-31.4.1.jar"> + <sha256 value="3cc17200f904645251cf6f67c1281a70691428cde8f417b9b6c54acf89bf1a8a" origin="Generated by Gradle"/> + </artifact> + <artifact name="protos-31.4.1.pom"> + <sha256 value="1e83c72c5c0c78aad51e5e2228c010db8f4cf60916476eab8e91f8a12e07e02b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.analytics-library" name="protos" version="31.4.2"> + <artifact name="protos-31.4.2.jar"> + <sha256 value="3cc17200f904645251cf6f67c1281a70691428cde8f417b9b6c54acf89bf1a8a" origin="Generated by Gradle"/> + </artifact> + <artifact name="protos-31.4.2.pom"> + <sha256 value="abfbc441e7115a0ad0960cb6d1930f9123815c4872998ceec00f9f82a9fd62ce" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.analytics-library" name="protos" version="31.5.0"> + <artifact name="protos-31.5.0.jar"> + <sha256 value="577abea11afdffd14688aa5fb0eabce17c74bcff86ca455738484bd91afacfb6" origin="Generated by Gradle"/> + </artifact> + <artifact name="protos-31.5.0.pom"> + <sha256 value="be963fccaf1e4a5a7f1b0d0712415fa36f3bfe61bf887e4edce1dcc3ccba1566" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.analytics-library" name="shared" version="31.4.1"> + <artifact name="shared-31.4.1.jar"> + <sha256 value="971e224c62bb2f05ca8f4f37ed29ac8f91914b39eb8ac620ba92a2e0ea38bc7d" origin="Generated by Gradle"/> + </artifact> + <artifact name="shared-31.4.1.pom"> + <sha256 value="8f027527a471a500aa8889bafef452d02ffde5b9776b3acce80db5d7ea2733bc" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.analytics-library" name="shared" version="31.4.2"> + <artifact name="shared-31.4.2.jar"> + <sha256 value="971e224c62bb2f05ca8f4f37ed29ac8f91914b39eb8ac620ba92a2e0ea38bc7d" origin="Generated by Gradle"/> + </artifact> + <artifact name="shared-31.4.2.pom"> + <sha256 value="407b3e55634f21acd256005d829e11aa88e6dba6a38ab01fdffea852bbc7bc54" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.analytics-library" name="shared" version="31.5.0"> + <artifact name="shared-31.5.0.jar"> + <sha256 value="324fc4983b14469a8df19fa5c2cc1bd3d1fd729554825f3912d8dfad8b8734d3" origin="Generated by Gradle"/> + </artifact> + <artifact name="shared-31.5.0.pom"> + <sha256 value="fb0179f5e5f88ed75849ee68ff014bdd2f80d69e8b4cff980c88d2fcb95647fd" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.analytics-library" name="tracker" version="31.4.1"> + <artifact name="tracker-31.4.1.jar"> + <sha256 value="542c6c0070a7df6254050532f2a14aa9db5553a0fc2cb203fdba7b721523e716" origin="Generated by Gradle"/> + </artifact> + <artifact name="tracker-31.4.1.pom"> + <sha256 value="929ea75f2b2f52f687e65a0c6b5c70b797ae1094ce376f7691cc885d35e07c63" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.analytics-library" name="tracker" version="31.4.2"> + <artifact name="tracker-31.4.2.jar"> + <sha256 value="542c6c0070a7df6254050532f2a14aa9db5553a0fc2cb203fdba7b721523e716" origin="Generated by Gradle"/> + </artifact> + <artifact name="tracker-31.4.2.pom"> + <sha256 value="759bcff005106e333240deee06449e2d19ccfe2c2be94288be7ee2c3c3d973cb" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.analytics-library" name="tracker" version="31.5.0"> + <artifact name="tracker-31.5.0.jar"> + <sha256 value="542c6c0070a7df6254050532f2a14aa9db5553a0fc2cb203fdba7b721523e716" origin="Generated by Gradle"/> + </artifact> + <artifact name="tracker-31.5.0.pom"> + <sha256 value="1c59c8a4ed6581117025b062811579dc3e18817a9c6cef8350391a838051869e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="aapt2" version="8.4.1-11315950"> + <artifact name="aapt2-8.4.1-11315950-linux.jar"> + <sha256 value="0436553853f613304254a570e67e24951da0b70d34e0594975a1d7df1e7af4a0" origin="Generated by Gradle"/> + </artifact> + <artifact name="aapt2-8.4.1-11315950-osx.jar"> + <sha256 value="57c0267a21c00801cf315a42c10d56fb4b56cad54da262ba52d3e34578f21226" origin="Generated by Gradle"/> + </artifact> + <artifact name="aapt2-8.4.1-11315950-windows.jar"> + <sha256 value="db296595cfdfce6137c5815701d87233e3df1032db205b2e7e59358af5623a60" origin="Generated by Gradle"/> + </artifact> + <artifact name="aapt2-8.4.1-11315950.pom"> + <sha256 value="6f298435c09dc14a210ff063fa0d048891d62e618498340dcbdcfcf62684897f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="aapt2" version="8.4.2-11315950"> + <artifact name="aapt2-8.4.2-11315950-linux.jar"> + <sha256 value="4857cca8b8210e986b56d38970f6a60dd05f04015e17c9606d8c8281dd926d6a" origin="Generated by Gradle"/> + </artifact> + <artifact name="aapt2-8.4.2-11315950-osx.jar"> + <sha256 value="9baf76499cd5db78e3ccae6c5f5f5986a1901bfda91cfc49e7c8268158b0ed93" origin="Generated by Gradle"/> + </artifact> + <artifact name="aapt2-8.4.2-11315950-windows.jar"> + <sha256 value="d9ce24ff0706d3745ace54c97ec29b7b25cf8036602865e4afffb1ec7d6af02c" origin="Generated by Gradle"/> + </artifact> + <artifact name="aapt2-8.4.2-11315950.pom"> + <sha256 value="bfed9acf31035df0fc41f43cf9c9812477c138a7900e736669c4aecaf72fcc29" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="aapt2" version="8.5.0-11315950"> + <artifact name="aapt2-8.5.0-11315950-linux.jar"> + <sha256 value="0e307c068433efbc645a1864d4380bc79911cf5de311a976f92040cb218739dc" origin="Generated by Gradle"/> + </artifact> + <artifact name="aapt2-8.5.0-11315950-osx.jar"> + <sha256 value="58d20fd3929e936db809b329998b4c37ff17c868a32a0b038c3db249f01ffe55" origin="Generated by Gradle"/> + </artifact> + <artifact name="aapt2-8.5.0-11315950-windows.jar"> + <sha256 value="eb563c350bae63dce8536c7136d9777429bd8a9d6a000cb670b2062dfce9b5a8" origin="Generated by Gradle"/> + </artifact> + <artifact name="aapt2-8.5.0-11315950.pom"> + <sha256 value="7d0451a80567e9563e7937ebb775195373cf88793d209ea90a38fde3c44e9ef0" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="aapt2-proto" version="8.4.1-11315950"> + <artifact name="aapt2-proto-8.4.1-11315950.jar"> + <sha256 value="1a7dfab0f1dbfe6ce4d6fad3d05927d208f8c3087cbc0eb2b761aebf48ea06e4" origin="Generated by Gradle"/> + </artifact> + <artifact name="aapt2-proto-8.4.1-11315950.module"> + <sha256 value="5c29e20b974943193b5b9e541d179f5ed4f456d081db2af1109270ddcbf2253f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="aapt2-proto" version="8.4.2-11315950"> + <artifact name="aapt2-proto-8.4.2-11315950.jar"> + <sha256 value="66a589c25b2aa3d9b3daa96f07f262e3a157e43e50024234ff419b0578439645" origin="Generated by Gradle"/> + </artifact> + <artifact name="aapt2-proto-8.4.2-11315950.module"> + <sha256 value="91253e883c553a0ff8fb01cf693bb2f955f8baa679a2b7c014b21a9a2f35871a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="aapt2-proto" version="8.5.0-11315950"> + <artifact name="aapt2-proto-8.5.0-11315950.jar"> + <sha256 value="b0c737bc5b14dc25b3a1d53559b661fef96e533fc34640b2a514b3518b53c06c" origin="Generated by Gradle"/> + </artifact> + <artifact name="aapt2-proto-8.5.0-11315950.module"> + <sha256 value="fdfceac7227b99c6c53fa2d781b4435e6b2dadedfd35d3b252c91fc87c0025a5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="aaptcompiler" version="8.4.1"> + <artifact name="aaptcompiler-8.4.1.jar"> + <sha256 value="476a4d68b56fba17d6da796f2b19ff7d176290bbb9bafee113fddf42c11595e0" origin="Generated by Gradle"/> + </artifact> + <artifact name="aaptcompiler-8.4.1.module"> + <sha256 value="79b5577a1d92ca45f6ec064fc2a054b4d4573401c9577149c27a195a31f5029d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="aaptcompiler" version="8.4.2"> + <artifact name="aaptcompiler-8.4.2.jar"> + <sha256 value="c8089ca4458d0cca55cd4e296b6d39d9f9208d3ad5efee58dad59b0a17b4f902" origin="Generated by Gradle"/> + </artifact> + <artifact name="aaptcompiler-8.4.2.module"> + <sha256 value="b1295ea15c8c5ed90bbd4895970985b5a5fbb1fcfca8401012c2414f12d1af30" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="aaptcompiler" version="8.5.0"> + <artifact name="aaptcompiler-8.5.0.jar"> + <sha256 value="16de28453ec07879beeccb7c13c9ecde7cd1ceca578148db362b7f88cf3db311" origin="Generated by Gradle"/> + </artifact> + <artifact name="aaptcompiler-8.5.0.module"> + <sha256 value="60ad2c8c3d3e4edffe95c3ce604ba469c691b75126e80c3ccb9e86ce1f50b6b0" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="apksig" version="8.4.1"> + <artifact name="apksig-8.4.1.jar"> + <sha256 value="c4f6fdf2148490296f422a7eb37647ea11d1c3b02f1f2349c33ce257f3c29b1f" origin="Generated by Gradle"/> + </artifact> + <artifact name="apksig-8.4.1.pom"> + <sha256 value="5ea065b142c34407796099de3200baeeaaf7b61ee46032eecb2c666c7cc68501" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="apksig" version="8.4.2"> + <artifact name="apksig-8.4.2.jar"> + <sha256 value="c4f6fdf2148490296f422a7eb37647ea11d1c3b02f1f2349c33ce257f3c29b1f" origin="Generated by Gradle"/> + </artifact> + <artifact name="apksig-8.4.2.pom"> + <sha256 value="c00e6da8e67aded0afe97db4891f502516a85c8faf0aa521e93a4b596db44aae" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="apksig" version="8.5.0"> + <artifact name="apksig-8.5.0.jar"> + <sha256 value="c4f6fdf2148490296f422a7eb37647ea11d1c3b02f1f2349c33ce257f3c29b1f" origin="Generated by Gradle"/> + </artifact> + <artifact name="apksig-8.5.0.pom"> + <sha256 value="04a5679eaa93ca79497ac089b2de63f600e5041528bc694a235470852743e9da" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="apkzlib" version="8.4.1"> + <artifact name="apkzlib-8.4.1.jar"> + <sha256 value="1c1a67d6f4f186427ac166ebaa0dd867f595d5144fc925252b05ffb9d1a156b7" origin="Generated by Gradle"/> + </artifact> + <artifact name="apkzlib-8.4.1.pom"> + <sha256 value="5238627b86886ba5871dbd5b020ffa7ebda2f517b3ad0a6f4a928222c556ee80" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="apkzlib" version="8.4.2"> + <artifact name="apkzlib-8.4.2.jar"> + <sha256 value="1c1a67d6f4f186427ac166ebaa0dd867f595d5144fc925252b05ffb9d1a156b7" origin="Generated by Gradle"/> + </artifact> + <artifact name="apkzlib-8.4.2.pom"> + <sha256 value="a6c80e38efebb4fccbf23a0ff1045473f3d07c50b4b73764a8489b1581b581aa" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="apkzlib" version="8.5.0"> + <artifact name="apkzlib-8.5.0.jar"> + <sha256 value="1c1a67d6f4f186427ac166ebaa0dd867f595d5144fc925252b05ffb9d1a156b7" origin="Generated by Gradle"/> + </artifact> + <artifact name="apkzlib-8.5.0.pom"> + <sha256 value="91427d8fa260a09a15d032d99ff862bb87c9f47470588385ddf2a0a40cf62985" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="builder" version="8.4.1"> + <artifact name="builder-8.4.1.jar"> + <sha256 value="27f8cd73e0ad60c2276c5d4e3c95d6f003fd02edd182ed77692dbbd13947449c" origin="Generated by Gradle"/> + </artifact> + <artifact name="builder-8.4.1.module"> + <sha256 value="4756386d6f1f1967e18739f9a7fddfc733fffda0c4643e5881a4a8467c1fdbdd" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="builder" version="8.4.2"> + <artifact name="builder-8.4.2.jar"> + <sha256 value="63868694a98cd0f7bf7c4effbf03da770f5b7e8cd734484f4e548f5dd6b9ca70" origin="Generated by Gradle"/> + </artifact> + <artifact name="builder-8.4.2.module"> + <sha256 value="eca844d401daf7da7be4ff2acfec69e7738890f3873cd25a633dc61d64ff2cf9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="builder" version="8.5.0"> + <artifact name="builder-8.5.0.jar"> + <sha256 value="06ca45826dd49604dc05b5986dea2e59977b01cfba3fd476e6728f906e76facf" origin="Generated by Gradle"/> + </artifact> + <artifact name="builder-8.5.0.module"> + <sha256 value="62613ff91cf6dcc7cd0b35c9039bea9a33949f55c5a001614e37c204e25c8a36" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="builder-model" version="8.4.1"> + <artifact name="builder-model-8.4.1.jar"> + <sha256 value="6b011ed07ffb13526dd91c0c1fbaf57bd45f893d01f44ba9902b13765515791f" origin="Generated by Gradle"/> + </artifact> + <artifact name="builder-model-8.4.1.module"> + <sha256 value="d2aff1a0123fde6c14208e0e8636788498b1959eb953310a6a45d3606d5bb098" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="builder-model" version="8.4.2"> + <artifact name="builder-model-8.4.2.jar"> + <sha256 value="8c94002357fe084f20440513685b31866e26f1e425dc6642a5f8c21522e39036" origin="Generated by Gradle"/> + </artifact> + <artifact name="builder-model-8.4.2.module"> + <sha256 value="886abeff84df16cfca31c161a4e07f75228960ee5ec9b236c6166b58317f437d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="builder-model" version="8.5.0"> + <artifact name="builder-model-8.5.0.jar"> + <sha256 value="a12b4a876c971b63ede6bdb7ec6d7706e8ad807c5fc094c693bd7c1bb7bf8b7e" origin="Generated by Gradle"/> + </artifact> + <artifact name="builder-model-8.5.0.module"> + <sha256 value="5379ed0daf571c8e94ab48ad9f34d4c18a499dfb9cf86cecbd69c9bbf0b359df" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="builder-test-api" version="8.4.1"> + <artifact name="builder-test-api-8.4.1.jar"> + <sha256 value="0695ffddad94a68d505ecf49ad6a26470a4227dea6129fc19e4ea5bdd9a769b9" origin="Generated by Gradle"/> + </artifact> + <artifact name="builder-test-api-8.4.1.module"> + <sha256 value="c4eb66b95ff2c191f878ba6b8c50fbb03540a0252b914e764886eeb74e50a215" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="builder-test-api" version="8.4.2"> + <artifact name="builder-test-api-8.4.2.jar"> + <sha256 value="fccc64f5021ac47adb4df373a20d8ed04b8982441142aa809b4697083cde0f79" origin="Generated by Gradle"/> + </artifact> + <artifact name="builder-test-api-8.4.2.module"> + <sha256 value="148b5b038e1b111d95c91259aab25d579b224185c022f4bec4e0988df26621bf" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="builder-test-api" version="8.5.0"> + <artifact name="builder-test-api-8.5.0.jar"> + <sha256 value="a4769c25ca5b540612d3e0e88254145c59dd053c76c2f106bc3f9cbc01954aae" origin="Generated by Gradle"/> + </artifact> + <artifact name="builder-test-api-8.5.0.module"> + <sha256 value="e80e6ed8eec3d53e402c8a64da65a8c50c170d767102d9d949eb7f4fde3c97c7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="bundletool" version="1.15.6"> + <artifact name="bundletool-1.15.6.jar"> + <sha256 value="f66992e537adb12ffa6d181cdc536d564b1c4b4b4c5972b4891706c6533a4641" origin="Generated by Gradle"/> + </artifact> + <artifact name="bundletool-1.15.6.pom"> + <sha256 value="745894f1d15599871b1c3a6cc09f0da03268f1373ce937ce35942b933d6518e6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="bundletool" version="1.16.0"> + <artifact name="bundletool-1.16.0.jar"> + <sha256 value="1ea2bf5274bbac7a3bb5618521d2fa11fb04e900e33a8a646029ec6332fc08c8" origin="Generated by Gradle"/> + </artifact> + <artifact name="bundletool-1.16.0.pom"> + <sha256 value="f2e8aad4455a42372462d5e18a26973d67571179a6757f3825f227e91af2f2db" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="gradle" version="8.4.1"> + <artifact name="gradle-8.4.1.jar"> + <sha256 value="a1bee785b6bb2aeb43740ecc24956ea75336272c88b72fb108f17938a6d9cec9" origin="Generated by Gradle"/> + </artifact> + <artifact name="gradle-8.4.1.module"> + <sha256 value="792bc97d885734db69582610c4af119697b3ba8425b4f2de4297dc4aa3ab2d7d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="gradle" version="8.4.2"> + <artifact name="gradle-8.4.2.jar"> + <sha256 value="8c5c2e91872c44cceeb4c16f3134bea71ce94802866a515fee98e191332753b5" origin="Generated by Gradle"/> + </artifact> + <artifact name="gradle-8.4.2.module"> + <sha256 value="707734318edd2ad1605a2e04d43e1ceb0e8aeb4880f2377641b65a7d5f38b990" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="gradle" version="8.5.0"> + <artifact name="gradle-8.5.0.jar"> + <sha256 value="0e8a3e285f9015baf4990fba1238bbceddf05e255967c3d5cec15b5deb52202c" origin="Generated by Gradle"/> + </artifact> + <artifact name="gradle-8.5.0.module"> + <sha256 value="2066397c8c3796db8e0b6fc5fe71cd99ae45ecef184d4b1a8e99de648db0d942" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="gradle-api" version="8.4.1"> + <artifact name="gradle-api-8.4.1.jar"> + <sha256 value="dbb375af8d47cf6b6820a34ce5cdb2dc6efcf33220d874e19caa3321ae00da61" origin="Generated by Gradle"/> + </artifact> + <artifact name="gradle-api-8.4.1.module"> + <sha256 value="e0d78dc9a6861a3b7599eb0d9415875ac68b934cee5988c86c4b6dc93b4eaac3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="gradle-api" version="8.4.2"> + <artifact name="gradle-api-8.4.2.jar"> + <sha256 value="c54df7dbff5e2e23922e7bb7df9e77709a425853e1af081b1d0824f829d8ee51" origin="Generated by Gradle"/> + </artifact> + <artifact name="gradle-api-8.4.2.module"> + <sha256 value="92ea3e08059f18a90bd2f85af97d9b8b223c6935b07cb4e7649390ee8b1909cc" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="gradle-api" version="8.5.0"> + <artifact name="gradle-api-8.5.0.jar"> + <sha256 value="dfbc62ea16b51e4d3a8417eb0af49c7e0516704e0d60a00f7c0ac8ca13e10db4" origin="Generated by Gradle"/> + </artifact> + <artifact name="gradle-api-8.5.0.module"> + <sha256 value="20189722d8d95111cde59c1d8c1bbc776891ac1a5de09421156b9e1c053bd1a2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="gradle-settings-api" version="8.4.1"> + <artifact name="gradle-settings-api-8.4.1.jar"> + <sha256 value="48fca54253cb93182afcb1173c1dbd9953aad7bf29b225b2e37ef9e1ddc01606" origin="Generated by Gradle"/> + </artifact> + <artifact name="gradle-settings-api-8.4.1.module"> + <sha256 value="5f9ccca03508351f095a3765a434590a415d69eb9106c110829affe27b8bbd05" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="gradle-settings-api" version="8.4.2"> + <artifact name="gradle-settings-api-8.4.2.jar"> + <sha256 value="80f945de76fa05578318dd0e5bfc9393583df0adb0425c0b384f50bfeb690c87" origin="Generated by Gradle"/> + </artifact> + <artifact name="gradle-settings-api-8.4.2.module"> + <sha256 value="5b5c2545e0cc2458287b3147a379d8580a8ae39fa9bd173926526a919b6c4c4d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="gradle-settings-api" version="8.5.0"> + <artifact name="gradle-settings-api-8.5.0.jar"> + <sha256 value="59ed9e273866bdd5beda9d6aa3bd2fdd9e8926d36b33614ab309261286f817f9" origin="Generated by Gradle"/> + </artifact> + <artifact name="gradle-settings-api-8.5.0.module"> + <sha256 value="5d3366c7b757cbd7287711439e4b97a2640d97d013e20d410887b3c98d52726f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="manifest-merger" version="31.4.1"> + <artifact name="manifest-merger-31.4.1.jar"> + <sha256 value="437c35776f2eb292cdfb809ac79da0028bc8b943d7f1f5e5095f0ccf13b1c66e" origin="Generated by Gradle"/> + </artifact> + <artifact name="manifest-merger-31.4.1.module"> + <sha256 value="f0547bef8c8786e5653705a5cee7a97727d314c63a6d4bb7b07074a06bec017f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="manifest-merger" version="31.4.2"> + <artifact name="manifest-merger-31.4.2.jar"> + <sha256 value="0b55cf1d17710a66cf0a229d6a560392a6320bcf7b6b3690e10cd90a4cad4694" origin="Generated by Gradle"/> + </artifact> + <artifact name="manifest-merger-31.4.2.module"> + <sha256 value="c76f18554f5963e79eabadfdb4105fad99ad2a140cb5625117ebefa8df96fc1b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="manifest-merger" version="31.5.0"> + <artifact name="manifest-merger-31.5.0.jar"> + <sha256 value="c3d7ff7cc87333c1fe34b5315bff14a85dede6bfb060d92ba483a99f7f39a4a4" origin="Generated by Gradle"/> + </artifact> + <artifact name="manifest-merger-31.5.0.module"> + <sha256 value="40db52f29158b5a16dc145cef02b3468cebbc1626e8b5a3805fdc5a0a9793c0d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build" name="transform-api" version="2.0.0-deprecated-use-gradle-api"> + <artifact name="transform-api-2.0.0-deprecated-use-gradle-api.jar"> + <sha256 value="4de4a3d05e1c534c2db9e4588bf34082bb2bd232d8abb9727c430290ce225740" origin="Generated by Gradle"/> + </artifact> + <artifact name="transform-api-2.0.0-deprecated-use-gradle-api.pom"> + <sha256 value="7c62f3856e8abca1d79257925f26c12668693f5d95904056bbac88605cfd8575" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build.jetifier" name="jetifier-core" version="1.0.0-beta10"> + <artifact name="jetifier-core-1.0.0-beta10.jar"> + <sha256 value="26abb4a13927d9062169c504c9e94fe80e9ae3a4f7b5ab8875ab007536a91f5e" origin="Generated by Gradle"/> + </artifact> + <artifact name="jetifier-core-1.0.0-beta10.module"> + <sha256 value="f0917589a42d276163f10040ab5842e91883dcbdb1d48bfd1f1fcaa72c1ca7b7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.build.jetifier" name="jetifier-processor" version="1.0.0-beta10"> + <artifact name="jetifier-processor-1.0.0-beta10.jar"> + <sha256 value="c5067a7b928237a1271a5e9cb5710e9f80b4973293945bc51e3a4c864ea4bfed" origin="Generated by Gradle"/> + </artifact> + <artifact name="jetifier-processor-1.0.0-beta10.module"> + <sha256 value="36c25576b19993df360170528cc62b7246c37776d6158154a67cdf8fc2d58e13" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.ddms" name="ddmlib" version="31.4.1"> + <artifact name="ddmlib-31.4.1.jar"> + <sha256 value="f98b88cfe0fe729d755f0bf6d5fc21e20f89aea9e630860dc0f75dcbec6fec0b" origin="Generated by Gradle"/> + </artifact> + <artifact name="ddmlib-31.4.1.pom"> + <sha256 value="17b53d1f91e16af0a086206668d36f7bdc97f3c00382a21b12d03ce12e65e0a5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.ddms" name="ddmlib" version="31.4.2"> + <artifact name="ddmlib-31.4.2.jar"> + <sha256 value="f98b88cfe0fe729d755f0bf6d5fc21e20f89aea9e630860dc0f75dcbec6fec0b" origin="Generated by Gradle"/> + </artifact> + <artifact name="ddmlib-31.4.2.pom"> + <sha256 value="67e58784d90b931e2fcdaf446f7e11bd1c39fad31403d68f5fa95bd75859281c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.ddms" name="ddmlib" version="31.5.0"> + <artifact name="ddmlib-31.5.0.jar"> + <sha256 value="c571c0224fe3e7e6607196d95cc3142fdc19f013e8f1510f68c7b4a686c53809" origin="Generated by Gradle"/> + </artifact> + <artifact name="ddmlib-31.5.0.pom"> + <sha256 value="e5a0906e9e236a09d633d230cbe988d37b69a6cd7d33435984414ecd25262121" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.emulator" name="proto" version="31.4.1"> + <artifact name="proto-31.4.1.jar"> + <sha256 value="5994f35bcfe498aee92a82340b7600902439b1aa153b9398591105c4e00bb994" origin="Generated by Gradle"/> + </artifact> + <artifact name="proto-31.4.1.pom"> + <sha256 value="bb427aa329f1180f4ad90029cf10544e0b0ec6dece7f851aa8c54a4199cb55ef" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.emulator" name="proto" version="31.4.2"> + <artifact name="proto-31.4.2.jar"> + <sha256 value="5994f35bcfe498aee92a82340b7600902439b1aa153b9398591105c4e00bb994" origin="Generated by Gradle"/> + </artifact> + <artifact name="proto-31.4.2.pom"> + <sha256 value="46804df4a4d51b2beb86021b9dc5fbfef9863db2055356f99ff24e91e5b250e3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.emulator" name="proto" version="31.5.0"> + <artifact name="proto-31.5.0.jar"> + <sha256 value="5994f35bcfe498aee92a82340b7600902439b1aa153b9398591105c4e00bb994" origin="Generated by Gradle"/> + </artifact> + <artifact name="proto-31.5.0.pom"> + <sha256 value="55855aaee229b78b483f00c41636ab383f9b00fac90ebc79828a5ccc8264d3de" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.external.com-intellij" name="intellij-core" version="31.4.1"> + <artifact name="intellij-core-31.4.1.jar"> + <sha256 value="025e5a282d9fb83a4e6073ff4ccd5a28e38f8a322a856d4aa29e4ad00bd377f4" origin="Generated by Gradle"/> + </artifact> + <artifact name="intellij-core-31.4.1.pom"> + <sha256 value="dc58aabaab1086cb5cbffc33a4e02044005387845865630243ad4e7a275f9d63" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.external.com-intellij" name="intellij-core" version="31.4.2"> + <artifact name="intellij-core-31.4.2.jar"> + <sha256 value="025e5a282d9fb83a4e6073ff4ccd5a28e38f8a322a856d4aa29e4ad00bd377f4" origin="Generated by Gradle"/> + </artifact> + <artifact name="intellij-core-31.4.2.pom"> + <sha256 value="20e5f3a00a4041b4ffeac8ce4feaccbe08defb8b8ad0857a2060d4caf9ba4101" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.external.com-intellij" name="intellij-core" version="31.5.0"> + <artifact name="intellij-core-31.5.0.jar"> + <sha256 value="4805b8e2ef4ba140b7b3d77ea619a752ff112bbd88a01a59219e04c01f76365d" origin="Generated by Gradle"/> + </artifact> + <artifact name="intellij-core-31.5.0.pom"> + <sha256 value="42095dd38a1226c71246ef771b880e29c5bedb9e6e7cb7c883c5993afbe4e66e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.external.com-intellij" name="kotlin-compiler" version="31.4.1"> + <artifact name="kotlin-compiler-31.4.1.jar"> + <sha256 value="fd45a1e4a45c51b46920445586f71c92a8801cb0880bcc0f485144ffbfbc284f" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-compiler-31.4.1.pom"> + <sha256 value="ab62e6a9583728a60fb57ab14b3866aef64e2ed97480306bc58d8e37070b6d50" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.external.com-intellij" name="kotlin-compiler" version="31.4.2"> + <artifact name="kotlin-compiler-31.4.2.jar"> + <sha256 value="fd45a1e4a45c51b46920445586f71c92a8801cb0880bcc0f485144ffbfbc284f" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-compiler-31.4.2.pom"> + <sha256 value="66001e0f67258ccbf4b03b9b4452adda35c2231f53e97d902647596bb9185bce" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.external.com-intellij" name="kotlin-compiler" version="31.5.0"> + <artifact name="kotlin-compiler-31.5.0.jar"> + <sha256 value="b1af44dd127b047e856e8c825fad69fbb0c5b9659d3420dc765688bc7a5384fc" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-compiler-31.5.0.pom"> + <sha256 value="a141b77c4e4e9d76d0cd9bde4722c2eb5fee4a58dd73682d0c7376bd7d06777e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.external.org-jetbrains" name="uast" version="31.4.1"> + <artifact name="uast-31.4.1.jar"> + <sha256 value="0df6547b37453f5ad2006179cd65b0ab7109b8f67b837d739509b2ff4f8310b3" origin="Generated by Gradle"/> + </artifact> + <artifact name="uast-31.4.1.pom"> + <sha256 value="85cde9c4969abb0cb72bc81fedb7cf436d4c22752ba3b3c53e48eb093104af09" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.external.org-jetbrains" name="uast" version="31.4.2"> + <artifact name="uast-31.4.2.jar"> + <sha256 value="0df6547b37453f5ad2006179cd65b0ab7109b8f67b837d739509b2ff4f8310b3" origin="Generated by Gradle"/> + </artifact> + <artifact name="uast-31.4.2.pom"> + <sha256 value="b34d785fbcba4d0daf4495c291669d07adf1a90a46a326779aaececa0aaaef62" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.external.org-jetbrains" name="uast" version="31.5.0"> + <artifact name="uast-31.5.0.jar"> + <sha256 value="3b11ae43800e0f82276664c313f9442c1a9ac208e3a0f01beb312e47079ccf8d" origin="Generated by Gradle"/> + </artifact> + <artifact name="uast-31.5.0.pom"> + <sha256 value="6d046c5db8e3ede52d1c788b335405edc27062785e67d150f3d43014d2923382" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.layoutlib" name="layoutlib-api" version="31.4.1"> + <artifact name="layoutlib-api-31.4.1.jar"> + <sha256 value="3c9064c3c3ab0e73f3a5953d1c11c57dbf57a3297957c1d7d460c92e14cf34b2" origin="Generated by Gradle"/> + </artifact> + <artifact name="layoutlib-api-31.4.1.pom"> + <sha256 value="645a10aa7565385dbe936ea700840bafa683756a4e7433fd8b0c5dda355131c2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.layoutlib" name="layoutlib-api" version="31.4.2"> + <artifact name="layoutlib-api-31.4.2.jar"> + <sha256 value="3c9064c3c3ab0e73f3a5953d1c11c57dbf57a3297957c1d7d460c92e14cf34b2" origin="Generated by Gradle"/> + </artifact> + <artifact name="layoutlib-api-31.4.2.pom"> + <sha256 value="488bd09f6c7a3e5933fca26ed79809ec5499779d44269d630fdd95175233c763" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.layoutlib" name="layoutlib-api" version="31.5.0"> + <artifact name="layoutlib-api-31.5.0.jar"> + <sha256 value="9271bf0b3fbde65e75a3e9c587701b8865d2f009ea159f2801ba98dbbc8ac360" origin="Generated by Gradle"/> + </artifact> + <artifact name="layoutlib-api-31.5.0.pom"> + <sha256 value="3035fb799d6919ed513c6e9ba2401245ab046fb06b6b8b4273ecef995e886713" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint" version="31.4.1"> + <artifact name="lint-31.4.1.jar"> + <sha256 value="4c8bbdfaa4fcddd5e8b8f3e9606057a1e5dd6ed8013a7bbc9ae2d4a7d950152c" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-31.4.1.pom"> + <sha256 value="a1fe4b0c158fdd0b287ff5f0d5247c245f910f0536ed5ce6bbabba11a975e1d1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint" version="31.4.2"> + <artifact name="lint-31.4.2.jar"> + <sha256 value="4c8bbdfaa4fcddd5e8b8f3e9606057a1e5dd6ed8013a7bbc9ae2d4a7d950152c" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-31.4.2.pom"> + <sha256 value="c94db770512556e8cf2613cb9bda52c2d0058ef58033291d246948be73abee1c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint" version="31.5.0"> + <artifact name="lint-31.5.0.jar"> + <sha256 value="757cdbddfbbc40fc40f4e2c3c70afc17a0b376f3a9c497c76652484652b1bd42" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-31.5.0.pom"> + <sha256 value="192bde1106453961b990afe9df0e6890dfaaa68a6b464e279c1edba93469edb2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint-api" version="31.4.1"> + <artifact name="lint-api-31.4.1.jar"> + <sha256 value="03046cddd2df9b0d25ae1f5260102922b9efc8118ad1c2566443a04e3008ee55" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-api-31.4.1.pom"> + <sha256 value="12f565e7c346b9ea1b2f227c9ecc23fadc1cbd4c9ae58c8ed208838052fc77cc" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint-api" version="31.4.2"> + <artifact name="lint-api-31.4.2.jar"> + <sha256 value="03046cddd2df9b0d25ae1f5260102922b9efc8118ad1c2566443a04e3008ee55" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-api-31.4.2.pom"> + <sha256 value="b2a6b08f9f6bdd02174b59e87ae5484fb02a9f2f7161f37e60cfad18237af2c3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint-api" version="31.5.0"> + <artifact name="lint-api-31.5.0.jar"> + <sha256 value="b4a39c6cbb22f2f4e168c3104453cc06543f61c5db3d5e636d7b9af9efc2df26" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-api-31.5.0.pom"> + <sha256 value="2d2b2de211c3afd19df4c55ec25bb984ca978fbdd9ffda6d3e86dfdaa63d1f1b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint-checks" version="31.4.1"> + <artifact name="lint-checks-31.4.1.jar"> + <sha256 value="51ad2a41dcf521ad7bb0de716ab7f13a065e3cafe69e9d94c95b54f8589992c6" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-checks-31.4.1.pom"> + <sha256 value="837582513ddbf393d00f6ef65daddbc1b92f0b5deca2c029b3d553c16d8f5619" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint-checks" version="31.4.2"> + <artifact name="lint-checks-31.4.2.jar"> + <sha256 value="51ad2a41dcf521ad7bb0de716ab7f13a065e3cafe69e9d94c95b54f8589992c6" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-checks-31.4.2.pom"> + <sha256 value="3fb9cbd850c1dca47a8ed2145105f5842b106df8114749b559294408ccd044d9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint-checks" version="31.5.0"> + <artifact name="lint-checks-31.5.0.jar"> + <sha256 value="5c80ce66f3babb7b663761a18211f741ca4f1009901b90b020c68003c288622b" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-checks-31.5.0.pom"> + <sha256 value="a611f94ff777f236e7660d7bf74b9f430ccdfc19fbc0264813a94410bb2b1ca7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint-gradle" version="31.4.1"> + <artifact name="lint-gradle-31.4.1.jar"> + <sha256 value="ec188c62447a6c7cd781072577b04470d7a8c321de926a95f5c7f92342060fbe" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-gradle-31.4.1.pom"> + <sha256 value="9bb40179435b5c937c3c00bc99cae4a4685eaf93fc559c8f2e904e4a8741dd49" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint-gradle" version="31.4.2"> + <artifact name="lint-gradle-31.4.2.jar"> + <sha256 value="ec188c62447a6c7cd781072577b04470d7a8c321de926a95f5c7f92342060fbe" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-gradle-31.4.2.pom"> + <sha256 value="c30be922c04b04a2e92e86c822e0d454760c91cd9d6ab4bade0cb45143731012" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint-gradle" version="31.5.0"> + <artifact name="lint-gradle-31.5.0.jar"> + <sha256 value="ec188c62447a6c7cd781072577b04470d7a8c321de926a95f5c7f92342060fbe" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-gradle-31.5.0.pom"> + <sha256 value="553d006beab96588b9708c3f708e656cf83fab51cf203f839cccde9c3cc4efa8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint-model" version="31.4.1"> + <artifact name="lint-model-31.4.1.jar"> + <sha256 value="ae5b38460f3a59c0725930d779dbbb3372f08031a1a41faa59d9f8500b3e4641" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-model-31.4.1.pom"> + <sha256 value="a73daa05a2d8f447e4e4589ae6b64af24f111fd12f7a3c1fbecf894cab1d7c96" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint-model" version="31.4.2"> + <artifact name="lint-model-31.4.2.jar"> + <sha256 value="ae5b38460f3a59c0725930d779dbbb3372f08031a1a41faa59d9f8500b3e4641" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-model-31.4.2.pom"> + <sha256 value="60a7b8acb8608d38fa3d7c9b9f1af4e9dfca6b6dc1e9cd1625c4e5b99617437a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint-model" version="31.5.0"> + <artifact name="lint-model-31.5.0.jar"> + <sha256 value="ae5b38460f3a59c0725930d779dbbb3372f08031a1a41faa59d9f8500b3e4641" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-model-31.5.0.pom"> + <sha256 value="5ce2a4f527e15fe5be3e19a36cc4bf9865e002c901b2e12a5d63efdb357512ac" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint-typedef-remover" version="31.4.1"> + <artifact name="lint-typedef-remover-31.4.1.jar"> + <sha256 value="5b4f485215ca4d86ef2319fc398b5f2251e62f5446bc5fd0e00653648ddde318" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-typedef-remover-31.4.1.pom"> + <sha256 value="8577f8f9bbe5f606e48d4cf1131adb4fefbecd6ca78a1b40367d9f552ef9b8c1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint-typedef-remover" version="31.4.2"> + <artifact name="lint-typedef-remover-31.4.2.jar"> + <sha256 value="5b4f485215ca4d86ef2319fc398b5f2251e62f5446bc5fd0e00653648ddde318" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-typedef-remover-31.4.2.pom"> + <sha256 value="d3963b9803ffe4195ce3ea750543419f832a7618f51354f735e6990fd35109a0" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.lint" name="lint-typedef-remover" version="31.5.0"> + <artifact name="lint-typedef-remover-31.5.0.jar"> + <sha256 value="5b4f485215ca4d86ef2319fc398b5f2251e62f5446bc5fd0e00653648ddde318" origin="Generated by Gradle"/> + </artifact> + <artifact name="lint-typedef-remover-31.5.0.pom"> + <sha256 value="7c07d1764691da3b505fb6b0214f0660d9462277a21b341be24efbfb18340491" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-device-provider-ddmlib" version="31.4.1"> + <artifact name="android-device-provider-ddmlib-31.4.1.jar"> + <sha256 value="a867f9be959ae8bf0bc2e773c93817559ee528b46ff733a37bc896687451b74e" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-device-provider-ddmlib-31.4.1.module"> + <sha256 value="9135fdb76500c2f2137edcc3c6de9ff1d6411fdf05f15c2d4d70d3ccc0f023d3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-device-provider-ddmlib" version="31.4.2"> + <artifact name="android-device-provider-ddmlib-31.4.2.jar"> + <sha256 value="4ae4712c2234e572188f19ef556e4ffc0a36b6384c496c83b3fa3afff955d164" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-device-provider-ddmlib-31.4.2.module"> + <sha256 value="7a98debf6e274ce6eea8ecea7e7a5686176c76fbc91e62ed3490c7690b1a94a8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-device-provider-ddmlib" version="31.5.0"> + <artifact name="android-device-provider-ddmlib-31.5.0.jar"> + <sha256 value="1d7e69619536727e606adbe6b914fcc8554130d7a6257fccefe71e249d7aa321" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-device-provider-ddmlib-31.5.0.module"> + <sha256 value="2dcb00d8111fa7de35122f3d0941fb2ba68e69b9464cc2f1c240f43e9dc83348" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-device-provider-ddmlib-proto" version="31.4.1"> + <artifact name="android-device-provider-ddmlib-proto-31.4.1.jar"> + <sha256 value="da9f3f3dae26544c90668549584765d5854a87c425d2cfe577cd34d3600ea097" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-device-provider-ddmlib-proto-31.4.1.pom"> + <sha256 value="b1fa32c46d8a9a291b010ed8747f924cbb15452cba3cdab3414de3ad6ae20c27" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-device-provider-ddmlib-proto" version="31.4.2"> + <artifact name="android-device-provider-ddmlib-proto-31.4.2.jar"> + <sha256 value="da9f3f3dae26544c90668549584765d5854a87c425d2cfe577cd34d3600ea097" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-device-provider-ddmlib-proto-31.4.2.pom"> + <sha256 value="eabda59558b7882dcabbc691c2a9aeab8e5b26547ed0f53fcdc4426ebe3363bb" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-device-provider-ddmlib-proto" version="31.5.0"> + <artifact name="android-device-provider-ddmlib-proto-31.5.0.jar"> + <sha256 value="da9f3f3dae26544c90668549584765d5854a87c425d2cfe577cd34d3600ea097" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-device-provider-ddmlib-proto-31.5.0.pom"> + <sha256 value="464265365803ee569f8c6f4ebe0ffadfa7090c084e7d0405b6f88f2cebcb965b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-device-provider-gradle" version="31.4.1"> + <artifact name="android-device-provider-gradle-31.4.1.jar"> + <sha256 value="dea40b48ee3aef01cdd9fc4d71e34f07cbb9e510d8573fe884b9af1500586948" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-device-provider-gradle-31.4.1.module"> + <sha256 value="a754e5d7547ce83716ef4d7b07c979a06c6040c9350cf780ef5257b27503ef2b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-device-provider-gradle" version="31.4.2"> + <artifact name="android-device-provider-gradle-31.4.2.jar"> + <sha256 value="a2edf5626992d7345c5a31fa1699c96dee0ebce521983df2ff49e4ebd1e646cc" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-device-provider-gradle-31.4.2.module"> + <sha256 value="0a3ff959c7abf492228348e3a861bfd7f2b677ef32a711ef63277e12ad32fa11" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-device-provider-gradle" version="31.5.0"> + <artifact name="android-device-provider-gradle-31.5.0.jar"> + <sha256 value="6e08736ccdb398283570a25989567d22796b107575446cb2d2e5dda7f6034361" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-device-provider-gradle-31.5.0.module"> + <sha256 value="d9b43868f52a89c53fd3713295e2a3e9c3ce8a5067baf6251204714352fa85ff" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-device-provider-gradle-proto" version="31.4.1"> + <artifact name="android-device-provider-gradle-proto-31.4.1.jar"> + <sha256 value="ad2342bb1d6f95563400a322493ea1c229cb93df3944f1261b7399f718494049" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-device-provider-gradle-proto-31.4.1.pom"> + <sha256 value="4acc834cfc40f273c3b7e28b05cd3ebc3287cf00dbecad809e6e7efbc8ae4f12" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-device-provider-gradle-proto" version="31.4.2"> + <artifact name="android-device-provider-gradle-proto-31.4.2.jar"> + <sha256 value="ad2342bb1d6f95563400a322493ea1c229cb93df3944f1261b7399f718494049" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-device-provider-gradle-proto-31.4.2.pom"> + <sha256 value="ca7f7532783b7ceae88cb1990c0242b4d44ab50df4a380b690bee47c3d46a72c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-device-provider-gradle-proto" version="31.5.0"> + <artifact name="android-device-provider-gradle-proto-31.5.0.jar"> + <sha256 value="ad2342bb1d6f95563400a322493ea1c229cb93df3944f1261b7399f718494049" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-device-provider-gradle-proto-31.5.0.pom"> + <sha256 value="9e98f3ea1493cbd322fe5bac1deb3cc32f6672256370d78611c5967093788635" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-additional-test-output" version="31.4.1"> + <artifact name="android-test-plugin-host-additional-test-output-31.4.1.jar"> + <sha256 value="1c047272109c223336a8cda7b37685a34658fabba2579e9cd6723fc454fc544e" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-additional-test-output-31.4.1.module"> + <sha256 value="53fa3eb5818f20f614d17c0035b5ac38fedebf782b0b584a978414be868ec145" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-additional-test-output" version="31.4.2"> + <artifact name="android-test-plugin-host-additional-test-output-31.4.2.jar"> + <sha256 value="63577a4528b610b7e441319ff2ec0c555cff0fe472b5d2df85e427f2e105ae65" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-additional-test-output-31.4.2.module"> + <sha256 value="85a7752d410163a5b12fbefdb7ee67d4f2e9556c11be28b67e27e2fdce8efd02" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-additional-test-output" version="31.5.0"> + <artifact name="android-test-plugin-host-additional-test-output-31.5.0.jar"> + <sha256 value="74aa46e5df2a72310343c795461a3ebfdd54dc7612cd283bf2e81d15f8d393c1" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-additional-test-output-31.5.0.module"> + <sha256 value="e049d5dc8937d42bb1763ee0c2c21e80055cf001efd617d828a0eb845b15fb2f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-additional-test-output-proto" version="31.4.1"> + <artifact name="android-test-plugin-host-additional-test-output-proto-31.4.1.jar"> + <sha256 value="38450694de6328c2c4cba696f9c04ecdd5ce6952355f68c3a22f9541d1d6546f" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-additional-test-output-proto-31.4.1.pom"> + <sha256 value="fe86680b236db1f113d5ee5cc1f55bef7b9efd4149c79425e8ae10d5876b12e8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-additional-test-output-proto" version="31.4.2"> + <artifact name="android-test-plugin-host-additional-test-output-proto-31.4.2.jar"> + <sha256 value="38450694de6328c2c4cba696f9c04ecdd5ce6952355f68c3a22f9541d1d6546f" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-additional-test-output-proto-31.4.2.pom"> + <sha256 value="916d4ca5edbb226bf7d7e288f63ec430aee464f7c29e610c9bc72dc05fa5f205" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-additional-test-output-proto" version="31.5.0"> + <artifact name="android-test-plugin-host-additional-test-output-proto-31.5.0.jar"> + <sha256 value="38450694de6328c2c4cba696f9c04ecdd5ce6952355f68c3a22f9541d1d6546f" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-additional-test-output-proto-31.5.0.pom"> + <sha256 value="45c383683880e4d28571f137f6e7256371cccc8846b487df292cc0676123708c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-apk-installer" version="31.4.1"> + <artifact name="android-test-plugin-host-apk-installer-31.4.1.jar"> + <sha256 value="20ef9f61f8fb1d54a9e16f0c496c08c3bbd70fb03c06e2a561d7a464fc04dc0e" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-apk-installer-31.4.1.module"> + <sha256 value="65a9f5416bdb01e7a17e94f6a4e47f20b2f4ac65557e8cbfcb3cf23268cf283b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-apk-installer" version="31.4.2"> + <artifact name="android-test-plugin-host-apk-installer-31.4.2.jar"> + <sha256 value="b278bfc7fb67a090756a795c7602c0a4800a9f39f58cfe77c4ba042810ee77b4" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-apk-installer-31.4.2.module"> + <sha256 value="17de0d963ee172f41811581315a379ec4a9ce65326ddc9330f5afb0d2a6d64be" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-apk-installer" version="31.5.0"> + <artifact name="android-test-plugin-host-apk-installer-31.5.0.jar"> + <sha256 value="7af9b0f162cf66fc5cb534096e2999673c0e5dfad93dd9d0966751020fbeafb3" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-apk-installer-31.5.0.module"> + <sha256 value="56e50e6c0416a6c2fa614b10dedccd817ef18d6607c659f76226eea971976dc0" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-apk-installer-proto" version="31.4.1"> + <artifact name="android-test-plugin-host-apk-installer-proto-31.4.1.jar"> + <sha256 value="ced49aca4031aa2c57b51d3909ba815082e563c1d9a246bc23ede973782524f7" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-apk-installer-proto-31.4.1.pom"> + <sha256 value="8f372d89f4724acb3106a8453b43894149287af94b4ca8c39dfe5cc331458066" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-apk-installer-proto" version="31.4.2"> + <artifact name="android-test-plugin-host-apk-installer-proto-31.4.2.jar"> + <sha256 value="ced49aca4031aa2c57b51d3909ba815082e563c1d9a246bc23ede973782524f7" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-apk-installer-proto-31.4.2.pom"> + <sha256 value="ff4127a1c8b7f55cdd12382c059ca2465317d98c126b1c20a8029eb3ed37dfa5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-apk-installer-proto" version="31.5.0"> + <artifact name="android-test-plugin-host-apk-installer-proto-31.5.0.jar"> + <sha256 value="ced49aca4031aa2c57b51d3909ba815082e563c1d9a246bc23ede973782524f7" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-apk-installer-proto-31.5.0.pom"> + <sha256 value="8a393c468956f56114b960c0601304684cf5728ab032b7cfbb7b9969e6f5de79" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-coverage" version="31.4.1"> + <artifact name="android-test-plugin-host-coverage-31.4.1.jar"> + <sha256 value="0603d01e9ce8930dd76bbc9c7d0188b65a5e6407547828be00b347add2bf44d9" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-coverage-31.4.1.module"> + <sha256 value="782a0ba6b54b15bcd722551bd9c54b4b96b883388447141da5ad9ca434ffcb0b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-coverage" version="31.4.2"> + <artifact name="android-test-plugin-host-coverage-31.4.2.jar"> + <sha256 value="350b55eb82483861aa26ddd108735470b179c5ebd22885303d442c15eeb57b4f" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-coverage-31.4.2.module"> + <sha256 value="56de4ebe47ce66ab45188099725b1f3fdf76844331143ad172245a64266c4c06" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-coverage" version="31.5.0"> + <artifact name="android-test-plugin-host-coverage-31.5.0.jar"> + <sha256 value="51c42f5a647f2e21ecb0abe022a31e13c38a0a0564727bfe3b9a17ec90603efa" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-coverage-31.5.0.module"> + <sha256 value="ff23ac9e7cb06abfaac72cbdf49d88b11673a9957015e6906062e265b1f80897" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-coverage-proto" version="31.4.1"> + <artifact name="android-test-plugin-host-coverage-proto-31.4.1.jar"> + <sha256 value="efb4d7014aaa7355246a07c2e437a2231fb252540ff1bce6872c88dc8da89d12" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-coverage-proto-31.4.1.pom"> + <sha256 value="f65a8e9c78e8cafb1604c4cfcd27935fce783c2d4325a314e5faef8942f1a46b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-coverage-proto" version="31.4.2"> + <artifact name="android-test-plugin-host-coverage-proto-31.4.2.jar"> + <sha256 value="efb4d7014aaa7355246a07c2e437a2231fb252540ff1bce6872c88dc8da89d12" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-coverage-proto-31.4.2.pom"> + <sha256 value="9669d2ca886a05726cd7fbb908224457758156ea51f04c9725fcbc0a5f49656d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-coverage-proto" version="31.5.0"> + <artifact name="android-test-plugin-host-coverage-proto-31.5.0.jar"> + <sha256 value="efb4d7014aaa7355246a07c2e437a2231fb252540ff1bce6872c88dc8da89d12" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-coverage-proto-31.5.0.pom"> + <sha256 value="d48104242b3cc77db79b05b1461a1fee066d4ae310793c70f32a4a6e228fd6da" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-device-info" version="31.4.1"> + <artifact name="android-test-plugin-host-device-info-31.4.1.jar"> + <sha256 value="88e393ac6ddafd872be2723a9a435995b01a1d5df0cdea0c235850e6fd42c2d6" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-device-info-31.4.1.module"> + <sha256 value="41119635a2e245c58b048c53c810fb43503576b4cc9cfa91ca807c8507b3b2a6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-device-info" version="31.4.2"> + <artifact name="android-test-plugin-host-device-info-31.4.2.jar"> + <sha256 value="9d63e4c7dbd7b0946e8583ea45e21e77ae25e322210217a1c5adc94ff7ce3ee3" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-device-info-31.4.2.module"> + <sha256 value="3c77e4a76fe704ac74fb2525c225f73d59cd48afb721cb08c728aebcaae41254" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-device-info" version="31.5.0"> + <artifact name="android-test-plugin-host-device-info-31.5.0.jar"> + <sha256 value="bae03af4164b43c3b88ca78bbaa25f86a89e9950da0cf54c3ff0b9596d2176f2" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-device-info-31.5.0.module"> + <sha256 value="39ea357e85a3a3045f13e4d3609b886481bb6015fbcd3b87b59079d8485b70fc" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-device-info-proto" version="31.4.1"> + <artifact name="android-test-plugin-host-device-info-proto-31.4.1.jar"> + <sha256 value="4f5eccc1d47a981b3ff120e98a3de0988214108fe9dc7b068e95a6b686ada495" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-device-info-proto-31.4.1.pom"> + <sha256 value="34df6c7afeaa0a951d0063c8a28b2122c52ab806c5c46a2a5b22993b62d0a0d3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-device-info-proto" version="31.4.2"> + <artifact name="android-test-plugin-host-device-info-proto-31.4.2.jar"> + <sha256 value="4f5eccc1d47a981b3ff120e98a3de0988214108fe9dc7b068e95a6b686ada495" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-device-info-proto-31.4.2.pom"> + <sha256 value="ad70c321107d1b6f4f59f31dfbc5d479621529821369eddf0481382be6f11057" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-device-info-proto" version="31.5.0"> + <artifact name="android-test-plugin-host-device-info-proto-31.5.0.jar"> + <sha256 value="4f5eccc1d47a981b3ff120e98a3de0988214108fe9dc7b068e95a6b686ada495" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-device-info-proto-31.5.0.pom"> + <sha256 value="859966cfca921409251c4f9ec9ad0b3b67fafdf6ed3a0bd278fff6fc4ab8552c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-emulator-control" version="31.4.1"> + <artifact name="android-test-plugin-host-emulator-control-31.4.1.jar"> + <sha256 value="3ef68c87cee8b53e8eb94a09726861877c244159de8582b5819856f070b87185" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-emulator-control-31.4.1.module"> + <sha256 value="09135470b1da10b8a7ea77ae090dea8cbff9bd59ab653e2080f72d570125bcd8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-emulator-control" version="31.4.2"> + <artifact name="android-test-plugin-host-emulator-control-31.4.2.jar"> + <sha256 value="42c97ac7ed16cc85e7449761c854da9cb2fad629ef7d6dc991bf525a1a588daa" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-emulator-control-31.4.2.module"> + <sha256 value="606724c21d5508c3d7d05565951f304dddfefea46a308640e4e926cf206f9383" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-emulator-control" version="31.5.0"> + <artifact name="android-test-plugin-host-emulator-control-31.5.0.jar"> + <sha256 value="18861f80dd6c8ebd906c9ca666cb602153be5239a6999d36fbbf6c6e88df616c" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-emulator-control-31.5.0.module"> + <sha256 value="fc1fcd63dd6aa2d922b540d4f0e08281548a3668b36e4c3a28ddc4e88cd26cf6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-emulator-control-proto" version="31.4.1"> + <artifact name="android-test-plugin-host-emulator-control-proto-31.4.1.jar"> + <sha256 value="aedec5ec4627d898cccdf42d8038db20eba4495753c53d1b0b378734491caf5f" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-emulator-control-proto-31.4.1.pom"> + <sha256 value="1fe77471ac5251e84f36305aa5412c6b1446f523e8d8e22d98231c697e531c8a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-emulator-control-proto" version="31.4.2"> + <artifact name="android-test-plugin-host-emulator-control-proto-31.4.2.jar"> + <sha256 value="aedec5ec4627d898cccdf42d8038db20eba4495753c53d1b0b378734491caf5f" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-emulator-control-proto-31.4.2.pom"> + <sha256 value="61df273d792b92c8cbd0e87f47418c94b558ccb0ea566e196e37a9aaffa6a44c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-emulator-control-proto" version="31.5.0"> + <artifact name="android-test-plugin-host-emulator-control-proto-31.5.0.jar"> + <sha256 value="aedec5ec4627d898cccdf42d8038db20eba4495753c53d1b0b378734491caf5f" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-emulator-control-proto-31.5.0.pom"> + <sha256 value="d924e3818ea908bdd071cfd4c1cc19e8a002fbd361bd252647a0e3ee58cb3b80" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-logcat" version="31.4.1"> + <artifact name="android-test-plugin-host-logcat-31.4.1.jar"> + <sha256 value="80b634bcf76e50497704da7e2281a96d3725bdc657dfbe252edf3897c5990f76" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-logcat-31.4.1.module"> + <sha256 value="c5f17631357f930020cb550727a8967b3f055f0462a477c913e76cf7afb6afd4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-logcat" version="31.4.2"> + <artifact name="android-test-plugin-host-logcat-31.4.2.jar"> + <sha256 value="e8f3205fa00ffd79649f883f4488b95b23766baa06f4dc6f05fdbcfd7d230e2a" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-logcat-31.4.2.module"> + <sha256 value="4e550a912d3b2e9f40bcdba399f040ad3a99b1a24ee006017d492e0663bfc9ba" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-logcat" version="31.5.0"> + <artifact name="android-test-plugin-host-logcat-31.5.0.jar"> + <sha256 value="d552a451a44613debf90993d0ca048cc0e2700145b0a79f53ff0ac343a0cd4dc" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-logcat-31.5.0.module"> + <sha256 value="073fd9597841ad9d9d4feedb9e727182f00af5c1906e856b9165ef9b19c88ec5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-logcat-proto" version="31.4.1"> + <artifact name="android-test-plugin-host-logcat-proto-31.4.1.jar"> + <sha256 value="9129024bd8e38353bca3eb26dfd8e3628e058d57d6e9d8451ffc35f016751e63" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-logcat-proto-31.4.1.pom"> + <sha256 value="db3154500bd1751edb430dd55ad05739facf4310742f215c8e057074c570d9a6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-logcat-proto" version="31.4.2"> + <artifact name="android-test-plugin-host-logcat-proto-31.4.2.jar"> + <sha256 value="9129024bd8e38353bca3eb26dfd8e3628e058d57d6e9d8451ffc35f016751e63" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-logcat-proto-31.4.2.pom"> + <sha256 value="d7f3d32f920cc8d5ca0c65f68d96fe5b725d6dab8b3fe4c0ada93d27ded3448e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-logcat-proto" version="31.5.0"> + <artifact name="android-test-plugin-host-logcat-proto-31.5.0.jar"> + <sha256 value="9129024bd8e38353bca3eb26dfd8e3628e058d57d6e9d8451ffc35f016751e63" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-logcat-proto-31.5.0.pom"> + <sha256 value="26893e645c359025731e50288408bc8876afcf84877c88c56ae13c17aeeefca2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-retention" version="31.4.1"> + <artifact name="android-test-plugin-host-retention-31.4.1.jar"> + <sha256 value="7a21b8d83545e2c91925f6650e5c0a5eb80d8f29b233e42215a4fe30562b806d" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-retention-31.4.1.module"> + <sha256 value="2de2f29c0fb6cdabe61cfae61bec2dad9bbe5307c01276e350f9940c5e512ba1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-retention" version="31.4.2"> + <artifact name="android-test-plugin-host-retention-31.4.2.jar"> + <sha256 value="be212be059e97ab0b08cee5a8e56b7e979ca3fdcacab5740e1b7c6cad4209a8f" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-retention-31.4.2.module"> + <sha256 value="c3327d23252090d42c7b331058a41a88a7a14d8158db42999107ba70c26e3308" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-retention" version="31.5.0"> + <artifact name="android-test-plugin-host-retention-31.5.0.jar"> + <sha256 value="d86d6bb692a8b4405418e864a84ec2c7c3753ac59ed571e0c026eb122422d95e" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-retention-31.5.0.module"> + <sha256 value="cac9c85a012f6edd26f927fc847af097594010fb9179215123192538ce475ccf" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-retention-proto" version="31.4.1"> + <artifact name="android-test-plugin-host-retention-proto-31.4.1.jar"> + <sha256 value="3db8ed38ef49b694caea466ae22e3b36cedc9557b3589d0e07c0cdd83294591c" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-retention-proto-31.4.1.pom"> + <sha256 value="e2ee2ad4972cb0de9c6e6838dac3991ac8c9da0df5b66b27cded5098332b3e49" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-retention-proto" version="31.4.2"> + <artifact name="android-test-plugin-host-retention-proto-31.4.2.jar"> + <sha256 value="3db8ed38ef49b694caea466ae22e3b36cedc9557b3589d0e07c0cdd83294591c" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-retention-proto-31.4.2.pom"> + <sha256 value="80bde90569b7e63c5dd4e6ab6206af53a7617620a8ab06d63c6b0a9248530db1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-host-retention-proto" version="31.5.0"> + <artifact name="android-test-plugin-host-retention-proto-31.5.0.jar"> + <sha256 value="3db8ed38ef49b694caea466ae22e3b36cedc9557b3589d0e07c0cdd83294591c" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-host-retention-proto-31.5.0.pom"> + <sha256 value="278156ce3fc6273dc0c16e119d8be34ee4a165092cedb8e946de5113e2d3db9a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-result-listener-gradle" version="31.4.1"> + <artifact name="android-test-plugin-result-listener-gradle-31.4.1.jar"> + <sha256 value="42c2a59ed10ca56f21eeb52dbaa436a1ecf121dfb97e484a1b545da8bb31173e" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-result-listener-gradle-31.4.1.module"> + <sha256 value="418c26c65eb6f9387397559c0fd57c55d7d7a3e3f8d175ad61a0a88c40b440c0" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-result-listener-gradle" version="31.4.2"> + <artifact name="android-test-plugin-result-listener-gradle-31.4.2.jar"> + <sha256 value="7271df2e627abf36ec710a1466dd8c59b133dc1c709524057f750049273dd494" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-result-listener-gradle-31.4.2.module"> + <sha256 value="87172d676a5aabfb597aab2ec4bafe4e7cac4c96257b467286a3f368a056e43b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-result-listener-gradle" version="31.5.0"> + <artifact name="android-test-plugin-result-listener-gradle-31.5.0.jar"> + <sha256 value="22bbd58eba2789edc2896cf79480c89dc07e8bbf7c88ecd82ace94b378160c10" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-result-listener-gradle-31.5.0.module"> + <sha256 value="e457d39b02bc8f23cef89f0d070fd838771729ee09e8253e5f9bc012b35583d9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-result-listener-gradle-proto" version="31.4.1"> + <artifact name="android-test-plugin-result-listener-gradle-proto-31.4.1.jar"> + <sha256 value="cbdf71bca60e14c30e7b2cf4b90f0f0a3e8c138f7cadd874b0d9c0ae082e0274" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-result-listener-gradle-proto-31.4.1.pom"> + <sha256 value="931443bdb0f1268cc3aa794643177d209efcd5a1afc6a0feae73b9f05d663f3a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-result-listener-gradle-proto" version="31.4.2"> + <artifact name="android-test-plugin-result-listener-gradle-proto-31.4.2.jar"> + <sha256 value="cbdf71bca60e14c30e7b2cf4b90f0f0a3e8c138f7cadd874b0d9c0ae082e0274" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-result-listener-gradle-proto-31.4.2.pom"> + <sha256 value="8c4e853bb8739b51d966aa5d54f039f155db27bf5c5e695966c30d36ae2c87e6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="android-test-plugin-result-listener-gradle-proto" version="31.5.0"> + <artifact name="android-test-plugin-result-listener-gradle-proto-31.5.0.jar"> + <sha256 value="cbdf71bca60e14c30e7b2cf4b90f0f0a3e8c138f7cadd874b0d9c0ae082e0274" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-result-listener-gradle-proto-31.5.0.pom"> + <sha256 value="a22a010610607be6a57334b8a44d4a185409583198c934e6ba36911b3ed3a559" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="utp-common" version="31.4.1"> + <artifact name="utp-common-31.4.1.jar"> + <sha256 value="bb150aea0c945d38f3360a7eb656d9e721b1cc6cddb36d50fe392cf7015762ce" origin="Generated by Gradle"/> + </artifact> + <artifact name="utp-common-31.4.1.pom"> + <sha256 value="f6a2b475d34fed77b61689ab9e54270d8c958cf29a0e79c55f029272a7a6522f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="utp-common" version="31.4.2"> + <artifact name="utp-common-31.4.2.jar"> + <sha256 value="bb150aea0c945d38f3360a7eb656d9e721b1cc6cddb36d50fe392cf7015762ce" origin="Generated by Gradle"/> + </artifact> + <artifact name="utp-common-31.4.2.pom"> + <sha256 value="d046b94a89f3c570297106e467f282abd122a3fd6b7344bd736c0ae81fd4bd74" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.android.tools.utp" name="utp-common" version="31.5.0"> + <artifact name="utp-common-31.5.0.jar"> + <sha256 value="bb150aea0c945d38f3360a7eb656d9e721b1cc6cddb36d50fe392cf7015762ce" origin="Generated by Gradle"/> + </artifact> + <artifact name="utp-common-31.5.0.pom"> + <sha256 value="2e3529810d646dacb01c45d62e31eda8df9b1acdbe20bf98dabc5435238d8b9b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.github.CanHub" name="Android-Image-Cropper" version="4.3.2"> + <artifact name="Android-Image-Cropper-4.3.2.aar"> + <sha256 value="970ebab04c4db12b56a8cf910e05c2155df243b43f2893768bb2ae1bdc3a467d" origin="Generated by Gradle"/> + </artifact> + <artifact name="Android-Image-Cropper-4.3.2.module"> + <sha256 value="4ec9f7fb834eaf3bbe3a586e48ca9262ddc8837735932dd3dffcf910e25a4243" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.github.MikeOrtiz" name="TouchImageView" version="3.6"> + <artifact name="TouchImageView-3.6.aar"> + <sha256 value="82f4d9cbe44b6b99e3af62733299da5bc4f25efba25da01d0b466856aae98f23" origin="Generated by Gradle"/> + </artifact> + <artifact name="TouchImageView-3.6.module"> + <sha256 value="607d0ebabb05336bdb48aec46cfdae2f684f4ff1d0ea0c65d0a62e033e9d7698" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.github.UnifiedPush" name="android-connector" version="2.4.0"> + <artifact name="android-connector-2.4.0.aar"> + <sha256 value="6ac50fd9acac605939bf818d0c2355ed783fbeef00ea351ed8e14cceb6ef2238" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-connector-2.4.0.module"> + <sha256 value="68e92058a57388f4513a0f87da4a6cf10fd1075e1fae55f75e7a3deaf63b8fff" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.github.bumptech.glide" name="annotations" version="4.16.0"> + <artifact name="annotations-4.16.0.jar"> + <sha256 value="c59e39b1b31b4d5592277cd90012149d740b569d4338f56443cac82c3fbda3d1" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotations-4.16.0.module"> + <sha256 value="9b918e3367e87d9a95899c306436df5e365ca2d04e4e9041740534fc8341c31f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.github.bumptech.glide" name="disklrucache" version="4.16.0"> + <artifact name="disklrucache-4.16.0-sources.jar"> + <sha256 value="0a4b3c7af0a83c4e2c798e2f77bcea48af5e76b86730e96ef38fd26488bfd9fa" origin="Generated by Gradle"/> + </artifact> + <artifact name="disklrucache-4.16.0.jar"> + <sha256 value="a227f6559c104aa5a5c88b0a70e9c7e3db6859e30b4774f3829121df9e627374" origin="Generated by Gradle"/> + </artifact> + <artifact name="disklrucache-4.16.0.pom"> + <sha256 value="a5e8df6f1d227c3e9be16fcbdfd3f533b21d8d03e2682add771af8f7cbbda944" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.github.bumptech.glide" name="gifdecoder" version="4.16.0"> + <artifact name="gifdecoder-4.16.0-sources.jar"> + <sha256 value="b0ba03d12b7ac21ce8ba9d2c0f241cb53f035dc0b762d2ca8af6af1da03d8b15" origin="Generated by Gradle"/> + </artifact> + <artifact name="gifdecoder-4.16.0.aar"> + <sha256 value="955f872af4d2a321fea2a346fc472225a2b99524acc46781491712e39b6a214f" origin="Generated by Gradle"/> + </artifact> + <artifact name="gifdecoder-4.16.0.pom"> + <sha256 value="30a608f777ca7720028c365dd670fb5c372d0e1151b9401680e3d09472db0017" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.github.bumptech.glide" name="glide" version="4.16.0"> + <artifact name="glide-4.16.0-sources.jar"> + <sha256 value="e2ea6d3e0707682b0109375dbe2a6fe2f7247c89b7519797e94cfb00249213b7" origin="Generated by Gradle"/> + </artifact> + <artifact name="glide-4.16.0.aar"> + <sha256 value="89811c63dd266a4851fd1b79c6fc0c982996ece435f5ff483fb830ca6cb53cd4" origin="Generated by Gradle"/> + </artifact> + <artifact name="glide-4.16.0.pom"> + <sha256 value="e72c8e310d7a2b9cee5acdcdb92fd2ef6394eb72e56a96cacb5692404702e571" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.github.bumptech.glide" name="ksp" version="4.16.0"> + <artifact name="ksp-4.16.0.jar"> + <sha256 value="45693dedc64a3f27ce30c1af5c979a551a71604935ec67b80c143870fee7c229" origin="Generated by Gradle"/> + </artifact> + <artifact name="ksp-4.16.0.module"> + <sha256 value="3386cd0ef152e4e3873200db7b7870ac2cff3f7e44a24dffc0add17937d6ed82" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.github.bumptech.glide" name="okhttp3-integration" version="4.16.0"> + <artifact name="okhttp3-integration-4.16.0-sources.jar"> + <sha256 value="bc024edafa5f3ac16c9e54331c4e240eac3f99129103d1bb4eb4e0d9ca331dae" origin="Generated by Gradle"/> + </artifact> + <artifact name="okhttp3-integration-4.16.0.aar"> + <sha256 value="5d5da82d36e2d4d2c1b92dc2cbcca883d9c14c2be8257a6fe70bcc38b64e9a96" origin="Generated by Gradle"/> + </artifact> + <artifact name="okhttp3-integration-4.16.0.pom"> + <sha256 value="c60137ec4e39d79217fa3acfb7d72cdc7c352be874fde306f992048e63435be5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.github.penfeizhou.android.animation" name="apng" version="2.23.0"> + <artifact name="apng-2.23.0-sources.jar"> + <sha256 value="a669e8da4927857f635a1b1ab1286df6bd955dfda38bd52a8f50d8ebb7dfb202" origin="Generated by Gradle"/> + </artifact> + <artifact name="apng-2.23.0.aar"> + <sha256 value="5586d8dfda1eed62b95cb01fe514beb1c525a57ebc02b9ad40f7aefbaf011232" origin="Generated by Gradle"/> + </artifact> + <artifact name="apng-2.23.0.pom"> + <sha256 value="768042a78f6364e574af9f854d35f69fbecf3c1c2fb32d22678b711731972fe1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.github.penfeizhou.android.animation" name="awebp" version="2.23.0"> + <artifact name="awebp-2.23.0-sources.jar"> + <sha256 value="199c074edeb96cf86e2f7101d86ac2577665ab6cf902c81073efd80ff27b2bd3" origin="Generated by Gradle"/> + </artifact> + <artifact name="awebp-2.23.0.aar"> + <sha256 value="9780e6e6935502f445d45fce4610e52f98a7f8bf723c82dfb91e2637281f7bf3" origin="Generated by Gradle"/> + </artifact> + <artifact name="awebp-2.23.0.pom"> + <sha256 value="3171babc67ca0341a3d3cb204ee728a93d058ccf27541cccab2ee8d968a56613" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.github.penfeizhou.android.animation" name="frameanimation" version="2.23.0"> + <artifact name="frameanimation-2.23.0-sources.jar"> + <sha256 value="aaddbd8c47241141af5051950d9e41f2c3ff8914eb0ee165eda9735007628eb6" origin="Generated by Gradle"/> + </artifact> + <artifact name="frameanimation-2.23.0.aar"> + <sha256 value="7b132ee73c77bcb1bfe44ea8f6fe4ef1315c1993d98e0081af749f4ae0dafe3e" origin="Generated by Gradle"/> + </artifact> + <artifact name="frameanimation-2.23.0.pom"> + <sha256 value="9e3ef6d2f7155468c78096183c6f4b371e5c273b616edb15ffdc35b4a5c8697c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.github.penfeizhou.android.animation" name="gif" version="2.23.0"> + <artifact name="gif-2.23.0-sources.jar"> + <sha256 value="494c9a24deca27b7296710639eb1de7fa9c340e5b3dda744637203e085858434" origin="Generated by Gradle"/> + </artifact> + <artifact name="gif-2.23.0.aar"> + <sha256 value="d6325c7491ac1c85c0c33f97ce6075974c9db264e2b5b3d9d1377b87216ef71f" origin="Generated by Gradle"/> + </artifact> + <artifact name="gif-2.23.0.pom"> + <sha256 value="fd29111e4591f079498765c66461b297a54fcbef75cd3b70c1d142b9d3186dc7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.github.penfeizhou.android.animation" name="glide-plugin" version="2.23.0"> + <artifact name="glide-plugin-2.23.0-sources.jar"> + <sha256 value="6e3a856d6f1957cd6b1cd8b935db6628793bf500f555b351dd1ba37f4723da53" origin="Generated by Gradle"/> + </artifact> + <artifact name="glide-plugin-2.23.0.aar"> + <sha256 value="8725a62abd5efa7e3262f738e1dfef4c26b580fc4cd07037c51b2f4506cd17f3" origin="Generated by Gradle"/> + </artifact> + <artifact name="glide-plugin-2.23.0.pom"> + <sha256 value="b7e1db7b75a07490e6985a55b4b2dce4f6f46d8c659de9453985fd0d44e55b2f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.android" name="annotations" version="4.1.1.4"> + <artifact name="annotations-4.1.1.4.jar"> + <sha256 value="ba734e1e84c09d615af6a09d33034b4f0442f8772dec120efb376d86a565ae15" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotations-4.1.1.4.pom"> + <sha256 value="e4bb54753c36a27a0e5d70154a5034fedd8feac4282295034bfd483d6c7aae78" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.android.material" name="material" version="1.12.0"> + <artifact name="material-1.12.0.aar"> + <sha256 value="4a672941b626b9ab91ae893ed22598ea53ad69125c858c0a59fa9b90daa5cb08" origin="Generated by Gradle"/> + </artifact> + <artifact name="material-1.12.0.module"> + <sha256 value="9ddba12b5c80444c45413f043b34fd3127c2493c506a9db1ecbfe63039d83e34" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.api.grpc" name="proto-google-common-protos" version="2.17.0"> + <artifact name="proto-google-common-protos-2.17.0.jar"> + <sha256 value="4ef1fe0c327fc1521d1d753b0b1c4a875a54bd14ebded3afff0ca395320b6ea9" origin="Generated by Gradle"/> + </artifact> + <artifact name="proto-google-common-protos-2.17.0.pom"> + <sha256 value="3f028153a585c59f558b3e43a7c9809a601a8bb5e91061d6c658fffa24cb8e26" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.auto" name="auto-common" version="0.11"> + <artifact name="auto-common-0.11.jar"> + <sha256 value="ec668cd50a3a66a5def17e6ae67423542e514181c0e9ab5b11959c0ac9c4222a" origin="Generated by Gradle"/> + </artifact> + <artifact name="auto-common-0.11.pom"> + <sha256 value="dee67bf309da1e0a241430de26bb7e762eff90de0826ff1c705d88208f5e1903" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.auto" name="auto-parent" version="6"> + <artifact name="auto-parent-6.pom"> + <sha256 value="05f740c6648165db00cf618dd56c200c4725e358e6d54f5853e0bec15734ea0a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.auto" name="auto-parent" version="7"> + <artifact name="auto-parent-7.pom"> + <sha256 value="a46426fccb5d32705ad9cbbc996f786bd048cc8cbdd21db046500169f15a4356" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.auto.service" name="auto-service-aggregator" version="1.0"> + <artifact name="auto-service-aggregator-1.0.pom"> + <sha256 value="c97acb57d385e70257643123aa824c7d343613fe93035529c0868e949cb60461" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.auto.service" name="auto-service-aggregator" version="1.0.1"> + <artifact name="auto-service-aggregator-1.0.1.pom"> + <sha256 value="0481460db59903362b26fabd3bdcd5ec200750f9391bbb6502fe3c7dc2df3e4c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.auto.service" name="auto-service-annotations" version="1.0"> + <artifact name="auto-service-annotations-1.0.pom"> + <sha256 value="9797b771f59106b9ab87a8cdd98a142173ad8e4790fc5e6bfc79316d8732c701" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.auto.service" name="auto-service-annotations" version="1.0.1"> + <artifact name="auto-service-annotations-1.0.1.jar"> + <sha256 value="c7bec54b7b5588b5967e870341091c5691181d954cf2039f1bf0a6eeb837473b" origin="Generated by Gradle"/> + </artifact> + <artifact name="auto-service-annotations-1.0.1.pom"> + <sha256 value="e9bbf1b3d66ca18004a6a07a20dbfa10ca2ce8367348f741de2fe8fefce68cc2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.auto.value" name="auto-value-annotations" version="1.10.4"> + <artifact name="auto-value-annotations-1.10.4-sources.jar"> + <sha256 value="61a433f015b12a6cf4ecff227c7748486ff8f294ffe9d39827b382ade0514d0a" origin="Generated by Gradle"/> + </artifact> + <artifact name="auto-value-annotations-1.10.4.jar"> + <sha256 value="e1c45e6beadaef9797cb0d9afd5a45621ad061cd8632012f85582853a3887825" origin="Generated by Gradle"/> + </artifact> + <artifact name="auto-value-annotations-1.10.4.pom"> + <sha256 value="73a5b8515f85f88c4089f7ff4a43cd1795a481fdab93fa9050b13cfa1a8d25f7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.auto.value" name="auto-value-annotations" version="1.6.2"> + <artifact name="auto-value-annotations-1.6.2.jar"> + <sha256 value="b48b04ddba40e8ac33bf036f06fc43995fc5084bd94bdaace807ce27d3bea3fb" origin="Generated by Gradle"/> + </artifact> + <artifact name="auto-value-annotations-1.6.2.pom"> + <sha256 value="1c76cd462fc96e7aa96dc70ce82f0d54063d6df16db35c9c7d9cc0d1a99d3fff" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.auto.value" name="auto-value-annotations" version="1.6.3"> + <artifact name="auto-value-annotations-1.6.3.jar"> + <sha256 value="0e951fee8c31f60270bc46553a8586001b7b93dbb12aec06373aa99a150392c0" origin="Generated by Gradle"/> + </artifact> + <artifact name="auto-value-annotations-1.6.3.pom"> + <sha256 value="e1fc780f7ee025e662b3da72723dbe2ac8dac0a2f8920f265315c4e1be3d765c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.auto.value" name="auto-value-parent" version="1.10.4"> + <artifact name="auto-value-parent-1.10.4.pom"> + <sha256 value="bec3a19e4ddc8b6406672333cc505b9e0cb6b3558bb242314869bb6e1d688d30" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.auto.value" name="auto-value-parent" version="1.6.2"> + <artifact name="auto-value-parent-1.6.2.pom"> + <sha256 value="27b640c82179f5cff62009c0b72033d9bc60f60e9902a66802274b7fe37fc81c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.auto.value" name="auto-value-parent" version="1.6.3"> + <artifact name="auto-value-parent-1.6.3.pom"> + <sha256 value="e59df5732b4cb34d5727181446f9ded9ce4425131a0a1062519eea196de4d8a1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.code.findbugs" name="jsr305" version="2.0.2"> + <artifact name="jsr305-2.0.2.jar"> + <sha256 value="1e7f53fa5b8b5c807e986ba335665da03f18d660802d8bf061823089d1bee468" origin="Generated by Gradle"/> + </artifact> + <artifact name="jsr305-2.0.2.pom"> + <sha256 value="8bc2c4f67a6396a7333dece2d1f991ca7d0aea48b29592265e2239be91972579" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.code.findbugs" name="jsr305" version="3.0.2"> + <artifact name="jsr305-3.0.2-sources.jar"> + <sha256 value="1c9e85e272d0708c6a591dc74828c71603053b48cc75ae83cce56912a2aa063b" origin="Generated by Gradle"/> + </artifact> + <artifact name="jsr305-3.0.2.jar"> + <sha256 value="766ad2a0783f2687962c8ad74ceecc38a28b9f72a2d085ee438b7813e928d0c7" origin="Generated by Gradle"/> + </artifact> + <artifact name="jsr305-3.0.2.pom"> + <sha256 value="19889dbdf1b254b2601a5ee645b8147a974644882297684c798afe5d63d78dfe" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.code.gson" name="gson" version="2.10.1"> + <artifact name="gson-2.10.1.jar"> + <sha256 value="4241c14a7727c34feea6507ec801318a3d4a90f070e4525681079fb94ee4c593" origin="Generated by Gradle"/> + </artifact> + <artifact name="gson-2.10.1.pom"> + <sha256 value="d2b115634f5c085db4b9c9ffc2658e89e231fdbfbe2242121a1cd95d4d948dd7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.code.gson" name="gson" version="2.9.0"> + <artifact name="gson-2.9.0.jar"> + <sha256 value="c96d60551331a196dac54b745aa642cd078ef89b6f267146b705f2c2cbef052d" origin="Generated by Gradle"/> + </artifact> + <artifact name="gson-2.9.0.pom"> + <sha256 value="7190d0b07f278e9f4c603f44e543940f81cf1a2559f851c6f298c9bb2be2978c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.code.gson" name="gson-parent" version="2.10.1"> + <artifact name="gson-parent-2.10.1.pom"> + <sha256 value="4248e0882426c615182385d6086c3ef3262e769957189e29306280b85482b833" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.code.gson" name="gson-parent" version="2.9.0"> + <artifact name="gson-parent-2.9.0.pom"> + <sha256 value="af781c9a5766ffea311a0df0536576a64decc661aa110c4de5c73ac8bf434424" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.crypto.tink" name="tink" version="1.7.0"> + <artifact name="tink-1.7.0.jar"> + <sha256 value="88970a456a08ba4c66b01b23e5846ca1095cc14e54cb48363e5d2e15a1307308" origin="Generated by Gradle"/> + </artifact> + <artifact name="tink-1.7.0.pom"> + <sha256 value="2aee3523715f8f2cd10b2603c8d19e561ac758310b7e2c9853946d2c5e7b4bf7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.dagger" name="dagger" version="2.28.3"> + <artifact name="dagger-2.28.3.jar"> + <sha256 value="f1dd23f8ae34a8e91366723991ead0d6499d1a3e9163ce550c200b02d76a872b" origin="Generated by Gradle"/> + </artifact> + <artifact name="dagger-2.28.3.pom"> + <sha256 value="265ba959a8e13c3a06133f04b539169c1018daffd4d33f53c453ab4cb386f570" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.dagger" name="dagger" version="2.51.1"> + <artifact name="dagger-2.51.1-sources.jar"> + <sha256 value="77e5cfde38b17973abca3f385bedf337e9f19a260665d5d8c447f34e46865cf6" origin="Generated by Gradle"/> + </artifact> + <artifact name="dagger-2.51.1.jar"> + <sha256 value="c3891a4c4a4e48682888ca321eaf8497004b286e1d9a2936867373219f7dd86d" origin="Generated by Gradle"/> + </artifact> + <artifact name="dagger-2.51.1.pom"> + <sha256 value="b74358f917a90d1ace285058b9588fc0545e85b4f6289059e77a7780e71de97d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.dagger" name="dagger-compiler" version="2.51.1"> + <artifact name="dagger-compiler-2.51.1.jar"> + <sha256 value="14cf2def1c4c8cd3b977840e297b463191d537cd1c8330992ca5c0b341a641ad" origin="Generated by Gradle"/> + </artifact> + <artifact name="dagger-compiler-2.51.1.pom"> + <sha256 value="101708ef85b7ffd8b34e66a022dd9d6f92bb26130bee38b0049db680cdd06a5f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.dagger" name="dagger-lint-aar" version="2.51.1"> + <artifact name="dagger-lint-aar-2.51.1.aar"> + <sha256 value="df2cce8977dd5fd4039377f59cd01ae845d95ccf7de431e8a4b0671b5d40e017" origin="Generated by Gradle"/> + </artifact> + <artifact name="dagger-lint-aar-2.51.1.pom"> + <sha256 value="22ff4f921c019307d62cdb7a2dd50ddc7d67dfeb2fe5dba3b607b7d7ab78dd85" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.dagger" name="dagger-spi" version="2.51.1"> + <artifact name="dagger-spi-2.51.1.jar"> + <sha256 value="deb52030b92b27c5dcd76b2c0747f1cf105b60939f6073b43eb06cfe7c9ba601" origin="Generated by Gradle"/> + </artifact> + <artifact name="dagger-spi-2.51.1.pom"> + <sha256 value="6a5a7219b32842cdd9e81bec248fc27bf4951b9d8e82df589567c2a5a5bcdc7e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.dagger" name="hilt-android" version="2.51.1"> + <artifact name="hilt-android-2.51.1-sources.jar"> + <sha256 value="0d03f9e2a8d227073e6cf89206713941930460d0b9e1c99665c4b9a08e19464f" origin="Generated by Gradle"/> + </artifact> + <artifact name="hilt-android-2.51.1.aar"> + <sha256 value="ede664ffc5ead06bb3ad722cf27a2b172d77b68b65da29de8053e9eb7c8e1832" origin="Generated by Gradle"/> + </artifact> + <artifact name="hilt-android-2.51.1.pom"> + <sha256 value="a82a80096058057162a3a720096d943fea78a657c9f49c25bdf151637cd0d52f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.dagger" name="hilt-android-gradle-plugin" version="2.51.1"> + <artifact name="hilt-android-gradle-plugin-2.51.1.jar"> + <sha256 value="cef1273ccfacb7b29f0330a7eb5dcb85cc049fcdc14090027043146011fd3ff1" origin="Generated by Gradle"/> + </artifact> + <artifact name="hilt-android-gradle-plugin-2.51.1.pom"> + <sha256 value="f5f5d74345a70ed771aec0f7c5a50e2df802b92fb874666ffbf1c8ada4ea6b41" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.dagger" name="hilt-compiler" version="2.51.1"> + <artifact name="hilt-compiler-2.51.1.jar"> + <sha256 value="d8f07ad8d9aedf4ea2fe0aab486321a27609a848f2df72885c416734ec17ed1f" origin="Generated by Gradle"/> + </artifact> + <artifact name="hilt-compiler-2.51.1.pom"> + <sha256 value="8b47332c547fe098b1c83bd00f226cdc83076496e598b61f2ffa4b1e67eecd0f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.dagger" name="hilt-core" version="2.51.1"> + <artifact name="hilt-core-2.51.1-sources.jar"> + <sha256 value="b813389b954ac99a5cc8b527a7bc7463f03500500009fa169092763605850d79" origin="Generated by Gradle"/> + </artifact> + <artifact name="hilt-core-2.51.1.jar"> + <sha256 value="1e4941b1df1e0a71bdfe9cbaad803579b48de73eb2c95a87f8279591ea52704d" origin="Generated by Gradle"/> + </artifact> + <artifact name="hilt-core-2.51.1.pom"> + <sha256 value="43b90b168c0e6e9b80f20d281391c9e5989f2182a0bf0cfcfa68c6f26ff031ff" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.dagger.hilt.android" name="com.google.dagger.hilt.android.gradle.plugin" version="2.51.1"> + <artifact name="com.google.dagger.hilt.android.gradle.plugin-2.51.1.pom"> + <sha256 value="25aecb846ed1054ca1226550e96e9a1695dfd7caa894ad7e7acb3824216d9459" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.devtools.ksp" name="com.google.devtools.ksp.gradle.plugin" version="2.0.0-1.0.22"> + <artifact name="com.google.devtools.ksp.gradle.plugin-2.0.0-1.0.22.pom"> + <sha256 value="50450fa2b0f0c57e7bc6daf7d4668115c48e169cd10530d39de14c5c2bce8fb0" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.devtools.ksp" name="symbol-processing" version="2.0.0-1.0.22"> + <artifact name="symbol-processing-2.0.0-1.0.22.jar"> + <sha256 value="84f37b2e285cde853e1d9cf1307766641b8bf9e411e264d736d4381e97e1316d" origin="Generated by Gradle"/> + </artifact> + <artifact name="symbol-processing-2.0.0-1.0.22.pom"> + <sha256 value="76ae10a3f07d2ea672f13189314e8572787858ed915ce5eed40724a7e4332cbc" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.devtools.ksp" name="symbol-processing-api" version="1.9.0-1.0.13"> + <artifact name="symbol-processing-api-1.9.0-1.0.13.module"> + <sha256 value="7bf557c8c69d638cd03eee46df4e0e2a9c6b26851383fa581c541dfd2eabfe1a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.devtools.ksp" name="symbol-processing-api" version="1.9.20-1.0.14"> + <artifact name="symbol-processing-api-1.9.20-1.0.14.module"> + <sha256 value="b351e1ea830783a903ecd55b100642ccf879b76db469d5d1b13b7bd757f36abb" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.devtools.ksp" name="symbol-processing-api" version="1.9.22-1.0.17"> + <artifact name="symbol-processing-api-1.9.22-1.0.17.jar"> + <sha256 value="3eec16833a02db45c4b55e1f76868567201499acce4b837fd2d3c84218da41cf" origin="Generated by Gradle"/> + </artifact> + <artifact name="symbol-processing-api-1.9.22-1.0.17.module"> + <sha256 value="16eb5828140e6f9ff29333b7bd42421367ee4f4e1427af30c5d9711362f70904" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.devtools.ksp" name="symbol-processing-api" version="2.0.0-1.0.22"> + <artifact name="symbol-processing-api-2.0.0-1.0.22.jar"> + <sha256 value="cec761da8521e0e369710c626ac048f77b891f65db0e1da1ad3d401901c901d5" origin="Generated by Gradle"/> + </artifact> + <artifact name="symbol-processing-api-2.0.0-1.0.22.module"> + <sha256 value="8f1e2b6512c5d28ce351731a487a9b740675006ad9d3ff4e042d00b34e3cc4e2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.devtools.ksp" name="symbol-processing-cmdline" version="2.0.0-1.0.22"> + <artifact name="symbol-processing-cmdline-2.0.0-1.0.22.jar"> + <sha256 value="9961400fb672e895e1c98963d493202207fd9918e38b3be27262be8b63e02d1d" origin="Generated by Gradle"/> + </artifact> + <artifact name="symbol-processing-cmdline-2.0.0-1.0.22.pom"> + <sha256 value="4dd113b6e7cf8fdf69111597e4a98599ac9dad9d3cdf4b918bbe517916d58f45" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.devtools.ksp" name="symbol-processing-common-deps" version="2.0.0-1.0.22"> + <artifact name="symbol-processing-common-deps-2.0.0-1.0.22.jar"> + <sha256 value="9109a3cf75c157cda8e81a04da064fbe19d64e5844695d1a4ef357ce507ac28d" origin="Generated by Gradle"/> + </artifact> + <artifact name="symbol-processing-common-deps-2.0.0-1.0.22.module"> + <sha256 value="8bb319bf72ba91e7f6709461d484527abb8623a0741faafa5cd03b164608de27" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.devtools.ksp" name="symbol-processing-gradle-plugin" version="2.0.0-1.0.22"> + <artifact name="symbol-processing-gradle-plugin-2.0.0-1.0.22.jar"> + <sha256 value="aa04d695caafd2ee1a1a061ae520a44e17594d5cce212c23337c6d1c52893988" origin="Generated by Gradle"/> + </artifact> + <artifact name="symbol-processing-gradle-plugin-2.0.0-1.0.22.module"> + <sha256 value="7a76f1dfb6d56a36d3840011c43a4327be05fb9e733371ba5151b5279296d475" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_annotation" version="2.19.1"> + <artifact name="error_prone_annotation-2.19.1.jar"> + <sha256 value="38c45cdaf993a7df6697703ca6ee00acbe376d3dade8d44daac3e33f039ca516" origin="Generated by Gradle"/> + </artifact> + <artifact name="error_prone_annotation-2.19.1.pom"> + <sha256 value="2a01226a8509dbd2b6da8008aaacf6fb4f83198ee1f2666cbabd4a19a3f98197" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_annotations" version="2.11.0"> + <artifact name="error_prone_annotations-2.11.0.pom"> + <sha256 value="0261ca01f2d2e9ac2ae2ece75d42c56323b385fb294b6bc943f62ef4e92ddf08" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_annotations" version="2.15.0"> + <artifact name="error_prone_annotations-2.15.0.jar"> + <sha256 value="067047714349e7789a5bdbfad9d1c0af9f3a1eb28c55a0ee3f68e682f905c4eb" origin="Generated by Gradle"/> + </artifact> + <artifact name="error_prone_annotations-2.15.0.pom"> + <sha256 value="7bae617e32681ebbb289c203c905f646c2a0397598af786d961a54aaa3d2b1e6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_annotations" version="2.18.0"> + <artifact name="error_prone_annotations-2.18.0.jar"> + <sha256 value="9e6814cb71816988a4fd1b07a993a8f21bb7058d522c162b1de849e19bea54ae" origin="Generated by Gradle"/> + </artifact> + <artifact name="error_prone_annotations-2.18.0.pom"> + <sha256 value="920135797dcca5917b5a5c017642a58d340a4cd1bcd12f56f892a5663bd7bddc" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_annotations" version="2.23.0"> + <artifact name="error_prone_annotations-2.23.0.jar"> + <sha256 value="ec6f39f068b6ff9ac323c68e28b9299f8c0a80ca512dccb1d4a70f40ac3ec054" origin="Generated by Gradle"/> + </artifact> + <artifact name="error_prone_annotations-2.23.0.pom"> + <sha256 value="d5abb17f231b63bf009358fa640281b744810cb9587e5994977834959c07dbd8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_annotations" version="2.25.0"> + <artifact name="error_prone_annotations-2.25.0-sources.jar"> + <sha256 value="fceb685146fad483d70fe78247e5e9b2a24af14fbae1f5ca940f58f8d2b05034" origin="Generated by Gradle"/> + </artifact> + <artifact name="error_prone_annotations-2.25.0.jar"> + <sha256 value="47d91c141efd6b1d9231a322deca8f4c32dac8e940c6c38d768b36add221700d" origin="Generated by Gradle"/> + </artifact> + <artifact name="error_prone_annotations-2.25.0.pom"> + <sha256 value="b58d79dee1968d58dce9c5b03e42106d7dd4c07f7078eb393c671e58f23bf611" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_annotations" version="2.3.1"> + <artifact name="error_prone_annotations-2.3.1.pom"> + <sha256 value="3edce6b711ba368efe16b9b7aacb0214fbd648414cb9b965953a2e7ed89a819a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_parent" version="2.11.0"> + <artifact name="error_prone_parent-2.11.0.pom"> + <sha256 value="8283f0cb44c624a79d330b6fd80b8b8a715a68b3685c9a951c3de837d4540551" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_parent" version="2.15.0"> + <artifact name="error_prone_parent-2.15.0.pom"> + <sha256 value="11dcacd17aaac69a99405273badfdaa5f9661d60d179b8a44f503958baa55f88" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_parent" version="2.18.0"> + <artifact name="error_prone_parent-2.18.0.pom"> + <sha256 value="47f22e99c7bf466391def16f8377985e5d3ba6f5bbcf65853644805513e15fad" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_parent" version="2.19.1"> + <artifact name="error_prone_parent-2.19.1.pom"> + <sha256 value="82f889260fe1d50ff6300f3b1977a7a80cd794f95f1b48ef210b85e4d16109cd" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_parent" version="2.23.0"> + <artifact name="error_prone_parent-2.23.0.pom"> + <sha256 value="f5470a4b3104fe309fbe94a80d16c3c54d20f748f4c5de4f68a428688f30cbd4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_parent" version="2.25.0"> + <artifact name="error_prone_parent-2.25.0.pom"> + <sha256 value="56753de4bea7a20c29d699667f29f29727c0bbad5cf69973a994094718da75c5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="error_prone_parent" version="2.3.1"> + <artifact name="error_prone_parent-2.3.1.pom"> + <sha256 value="767525d9a81129cd081968382980336327be4162b1e2251a182911daa733c123" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.errorprone" name="javac-shaded" version="9-dev-r4023-3"> + <artifact name="javac-shaded-9-dev-r4023-3.jar"> + <sha256 value="65bfccf60986c47fbc17c9ebab0be626afc41741e0a6ec7109e0768817a36f30" origin="Generated by Gradle"/> + </artifact> + <artifact name="javac-shaded-9-dev-r4023-3.pom"> + <sha256 value="7459fd63c1e73770ca44d37a7a685b731a946eb7cd701ccb284dcb0ce6de3f88" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.flatbuffers" name="flatbuffers-java" version="1.12.0"> + <artifact name="flatbuffers-java-1.12.0.jar"> + <sha256 value="3f8c088b4dd04a9858721f2e162508c94db0dd86f961e306ee63ef2eda871bf7" origin="Generated by Gradle"/> + </artifact> + <artifact name="flatbuffers-java-1.12.0.pom"> + <sha256 value="cb226baf546260770f21e8152a6aa88ba15230d739f750df480f2a668d43e0eb" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.googlejavaformat" name="google-java-format" version="1.5"> + <artifact name="google-java-format-1.5.jar"> + <sha256 value="aa19ad7850fb85178aa22f2fddb163b84d6ce4d0035872f30d4408195ca1144e" origin="Generated by Gradle"/> + </artifact> + <artifact name="google-java-format-1.5.pom"> + <sha256 value="994510ba3b16fb02e5ca17e8fd6158e2702fe95a1359c80be547b86b76a7aad5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.googlejavaformat" name="google-java-format-parent" version="1.5"> + <artifact name="google-java-format-parent-1.5.pom"> + <sha256 value="13aaf29158343f8b9c7dd7d3f58610290b05ad29ea69c7b9504869e47fbf6319" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.guava" name="failureaccess" version="1.0.1"> + <artifact name="failureaccess-1.0.1.jar"> + <sha256 value="a171ee4c734dd2da837e4b16be9df4661afab72a41adaf31eb84dfdaf936ca26" origin="Generated by Gradle"/> + </artifact> + <artifact name="failureaccess-1.0.1.pom"> + <sha256 value="e96042ce78fecba0da2be964522947c87b40a291b5fd3cd672a434924103c4b9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.guava" name="failureaccess" version="1.0.2"> + <artifact name="failureaccess-1.0.2.jar"> + <sha256 value="8a8f81cf9b359e3f6dfa691a1e776985c061ef2f223c9b2c80753e1b458e8064" origin="Generated by Gradle"/> + </artifact> + <artifact name="failureaccess-1.0.2.pom"> + <sha256 value="19ebc6f4bdb4edbb3d07b6ee994f846b54ef295582a9b5634719ffa9f31d03b2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.guava" name="guava" version="30.1.1-jre"> + <artifact name="guava-30.1.1-jre.pom"> + <sha256 value="6d18c9188ad4b7855fb7fea6f1793754b41fa1747811ae1e3d753d6fcc9dcc59" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.guava" name="guava" version="32.0.1-jre"> + <artifact name="guava-32.0.1-jre.jar"> + <sha256 value="bd7fa227591fb8509677d0d1122cf95158f3b8a9f45653f58281d879f6dc48c5" origin="Generated by Gradle"/> + </artifact> + <artifact name="guava-32.0.1-jre.pom"> + <sha256 value="42c257f7f736d377b31afeeee978ab26d730cd70af60dde7662e182352e2482a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.guava" name="guava" version="32.1.3-android"> + <artifact name="guava-32.1.3-android.jar"> + <sha256 value="20e6ac8902ddf49e7806cc70f3054c8d91accb5eefdc10f3207e80e0a336b263" origin="Generated by Gradle"/> + </artifact> + <artifact name="guava-32.1.3-android.module"> + <sha256 value="f8a87cef72c0e8027a6fae3cbdf0072a241ce6d8e760f96e7087b742a6605a61" origin="Generated by Gradle"/> + </artifact> + <artifact name="guava-32.1.3-jre.jar"> + <sha256 value="6d4e2b5a118aab62e6e5e29d185a0224eed82c85c40ac3d33cf04a270c3b3744" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.guava" name="guava" version="33.0.0-android"> + <artifact name="guava-33.0.0-android.jar"> + <sha256 value="640535014a304c50221abad19d6b22a096f13119c7c447a655cc1e009a32208d" origin="Generated by Gradle"/> + </artifact> + <artifact name="guava-33.0.0-android.module"> + <sha256 value="15572b680b5dfcbef966d0120e6504129a8c7fbf600845df369d939317b82036" origin="Generated by Gradle"/> + </artifact> + <artifact name="guava-33.0.0-jre.jar"> + <sha256 value="f4d85c3e4d411694337cb873abea09b242b664bb013320be6105327c45991537" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.guava" name="guava" version="33.0.0-jre"> + <artifact name="guava-33.0.0-jre.jar"> + <sha256 value="f4d85c3e4d411694337cb873abea09b242b664bb013320be6105327c45991537" origin="Generated by Gradle"/> + </artifact> + <artifact name="guava-33.0.0-jre.module"> + <sha256 value="59a2dbd055d1baa762e78f1a5ba3ab973edd13fc27dcc1871105e2f797f682d3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.guava" name="guava-parent" version="26.0-android"> + <artifact name="guava-parent-26.0-android.pom"> + <sha256 value="f8698ab46ca996ce889c1afc8ca4f25eb8ac6b034dc898d4583742360016cc04" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.guava" name="guava-parent" version="30.1.1-jre"> + <artifact name="guava-parent-30.1.1-jre.pom"> + <sha256 value="0422bd45ca2497bfa18aad2698324965ed70da0907b8a7d459b7ab3b5eed3834" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.guava" name="guava-parent" version="32.0.1-jre"> + <artifact name="guava-parent-32.0.1-jre.pom"> + <sha256 value="43ed0e36b353f41e5eb75cd756667c9e2df97cef06eb16066967158a1d034d2a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.guava" name="guava-parent" version="32.1.3-android"> + <artifact name="guava-parent-32.1.3-android.pom"> + <sha256 value="41fc26464fba8ab0b55b402676b45369e0aaa9ba8e0a5a5dec80452908ac97e1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.guava" name="guava-parent" version="33.0.0-android"> + <artifact name="guava-parent-33.0.0-android.pom"> + <sha256 value="5af483b78c49de78c49faedd2a58752784a44c8363f5e25e52e297814c09177a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.guava" name="guava-parent" version="33.0.0-jre"> + <artifact name="guava-parent-33.0.0-jre.pom"> + <sha256 value="040cc88c680b413d70ba9fc8371b36093021b10996aa3598621de767d418229a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.guava" name="listenablefuture" version="1.0"> + <artifact name="listenablefuture-1.0-sources.jar"> + <sha256 value="3d1bd8d4a39b293612a40e547ec51d9ce34fa638d7adeae83871cdbe2923b161" origin="Generated by Gradle"/> + </artifact> + <artifact name="listenablefuture-1.0.jar"> + <sha256 value="e4ad7607e5c0477c6f890ef26a49cb8d1bb4dffb650bab4502afee64644e3069" origin="Generated by Gradle"/> + </artifact> + <artifact name="listenablefuture-1.0.pom"> + <sha256 value="53873caf26bc1ed8a567ea6c939ab2aaa3f47a5e32d5cade95ddf5080d23238a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.guava" name="listenablefuture" version="9999.0-empty-to-avoid-conflict-with-guava"> + <artifact name="listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar"> + <sha256 value="b372a037d4230aa57fbeffdef30fd6123f9c0c2db85d0aced00c91b974f33f99" origin="Generated by Gradle"/> + </artifact> + <artifact name="listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.pom"> + <sha256 value="18d4b1db26153d4e55079ce1f76bb1fe05cdb862ef9954a88cbcc4ff38b8679b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.j2objc" name="j2objc-annotations" version="1.3"> + <artifact name="j2objc-annotations-1.3.pom"> + <sha256 value="5faca824ba115bee458730337dfdb2fcea46ba2fd774d4304edbf30fa6a3f055" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.j2objc" name="j2objc-annotations" version="2.8"> + <artifact name="j2objc-annotations-2.8.jar"> + <sha256 value="f02a95fa1a5e95edb3ed859fd0fb7df709d121a35290eff8b74dce2ab7f4d6ed" origin="Generated by Gradle"/> + </artifact> + <artifact name="j2objc-annotations-2.8.pom"> + <sha256 value="37f87798b18385113c918bfa9e1276fe50735ef8fa849b5800c519d54dbf11f8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.jimfs" name="jimfs" version="1.1"> + <artifact name="jimfs-1.1.jar"> + <sha256 value="c4828e28d7c0a930af9387510b3bada7daa5c04d7c25a75c7b8b081f1c257ddd" origin="Generated by Gradle"/> + </artifact> + <artifact name="jimfs-1.1.pom"> + <sha256 value="efa86e5cd922f17b472fdfcae57234d8d4ac3e148b6250737dfce454af7a7a44" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.jimfs" name="jimfs-parent" version="1.1"> + <artifact name="jimfs-parent-1.1.pom"> + <sha256 value="c71555751e57e0ef912870e8ac9625ae782502a6a5b9c19ccf83b2a97d8b26bd" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.protobuf" name="protobuf-bom" version="3.22.3"> + <artifact name="protobuf-bom-3.22.3.pom"> + <sha256 value="13a32dfb9de6fc1c3c3f7af53e4d5c77fd77d2b476bae38b74b7581cdee2e655" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.protobuf" name="protobuf-java" version="3.22.3"> + <artifact name="protobuf-java-3.22.3.jar"> + <sha256 value="59d388ea6a2d2d76ae8efff7fd4d0c60c6f0f464c3d3ab9be8e5add092975708" origin="Generated by Gradle"/> + </artifact> + <artifact name="protobuf-java-3.22.3.pom"> + <sha256 value="186ea794150f5b42aea7ec6041df373d1d8a8a831624f58a55debb6043ec7312" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.protobuf" name="protobuf-java-util" version="3.22.3"> + <artifact name="protobuf-java-util-3.22.3.jar"> + <sha256 value="c615f76879dc5c303e4df5b94a6afa39534058c7545db2d483fd95d9f63c8bfe" origin="Generated by Gradle"/> + </artifact> + <artifact name="protobuf-java-util-3.22.3.pom"> + <sha256 value="b44701b06a064865ec9b5614a93e9e28fadd7d4dfc4b460f21c819ef53dbe2d6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.protobuf" name="protobuf-parent" version="3.22.3"> + <artifact name="protobuf-parent-3.22.3.pom"> + <sha256 value="399133d7f6f57934dd76c4b18e86348f424532108daf4a01c8f820b8665f0929" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.testing.platform" name="android-device-provider-local" version="0.0.9-alpha02"> + <artifact name="android-device-provider-local-0.0.9-alpha02.jar"> + <sha256 value="446d0fca4e3711e3e37219cf888ec12b4dec3f14999ee439735385fb787e914b" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-device-provider-local-0.0.9-alpha02.pom"> + <sha256 value="afd953a41e294a6094c02620c8e9a0ebc9a743a00ff44a9db5b84f29497730ba" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.testing.platform" name="android-driver-instrumentation" version="0.0.9-alpha02"> + <artifact name="android-driver-instrumentation-0.0.9-alpha02.jar"> + <sha256 value="a34731dc23ead15144ad100efac5726a7ed82c2ec8b889504d549ae72dff5029" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-driver-instrumentation-0.0.9-alpha02.pom"> + <sha256 value="722d8c31107b71baf78f11fb10e879c7158f4681d99bacade726147368dc6ade" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.testing.platform" name="android-test-plugin" version="0.0.9-alpha02"> + <artifact name="android-test-plugin-0.0.9-alpha02.jar"> + <sha256 value="a87ed1c766c9a498cfc9280ca65d0c4853875b7ed7fa5ffd89428904eba1c0ff" origin="Generated by Gradle"/> + </artifact> + <artifact name="android-test-plugin-0.0.9-alpha02.pom"> + <sha256 value="70eb4ad0de146f958eb5d99bdff8d72b7b07a5c27ac690c421ae45617e031e54" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.testing.platform" name="core" version="0.0.9-alpha02"> + <artifact name="core-0.0.9-alpha02.jar"> + <sha256 value="fe215abd101f29fb388535592e1f14bb4cf8501da6db84422ba1945ac4e26872" origin="Generated by Gradle"/> + </artifact> + <artifact name="core-0.0.9-alpha02.pom"> + <sha256 value="963e12fc1a9e94ca091ded5aac611284d36cf0e309ee71882813b5a81183098a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.testing.platform" name="core-proto" version="0.0.9-alpha02"> + <artifact name="core-proto-0.0.9-alpha02.jar"> + <sha256 value="6d8a8906774150f43a8fad08ca64e25c6070c39bd8a6fc13b2593f289242fe95" origin="Generated by Gradle"/> + </artifact> + <artifact name="core-proto-0.0.9-alpha02.pom"> + <sha256 value="27ce7959427a2ffee48d0feb45128a3f36cc417ed8a822afa22822c8f5a58b25" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.testing.platform" name="launcher" version="0.0.9-alpha02"> + <artifact name="launcher-0.0.9-alpha02.jar"> + <sha256 value="5423e8ca410fcdbcfb3286c18b8be080eaf4ee9bd796ead903e8c885ead16ec3" origin="Generated by Gradle"/> + </artifact> + <artifact name="launcher-0.0.9-alpha02.pom"> + <sha256 value="0f022a32490b76449f7404b0506dace458aeaa97c8300b34cfd5be1af1ac992b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.truth" name="truth" version="1.4.2"> + <artifact name="truth-1.4.2-sources.jar"> + <sha256 value="9e6e49a3d2eefcadc0294878cb19fa6c6da305f2939c422f3cbd8caf9efe80bb" origin="Generated by Gradle"/> + </artifact> + <artifact name="truth-1.4.2.jar"> + <sha256 value="14c297bc64ca8bc15b6baf67f160627e4562ec91624797e312e907b431113508" origin="Generated by Gradle"/> + </artifact> + <artifact name="truth-1.4.2.pom"> + <sha256 value="8ec82a5568cf95890cc85ef30eb2941c42870c5136a3e0b18d50cb3956584000" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.google.truth" name="truth-parent" version="1.4.2"> + <artifact name="truth-parent-1.4.2.pom"> + <sha256 value="1065626a84aaa9f228c37fc43b53c3e5901a344e2b31feea0115e10e7b130d17" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.googlecode.juniversalchardet" name="juniversalchardet" version="1.0.3"> + <artifact name="juniversalchardet-1.0.3.jar"> + <sha256 value="757bfe906193b8b651e79dc26cd67d6b55d0770a2cdfb0381591504f779d4a76" origin="Generated by Gradle"/> + </artifact> + <artifact name="juniversalchardet-1.0.3.pom"> + <sha256 value="7846399b35c7cd642a9b3a000c3e2d62d04eb37a4547b6933cc8b18bcc2f086b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.gradle" name="develocity-gradle-plugin" version="3.17.4"> + <artifact name="develocity-gradle-plugin-3.17.4.jar"> + <sha256 value="e2b3f8a191b0b401b75c2c4542d3d1719814a4212e6920fae4f2f940678bfd99" origin="Generated by Gradle"/> + </artifact> + <artifact name="develocity-gradle-plugin-3.17.4.module"> + <sha256 value="c2bce4ef3437b8cb31b5b00066094c67e33274dd846a17329dc4d5ceadcad56c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.gradle" name="develocity-gradle-plugin" version="3.17.5"> + <artifact name="develocity-gradle-plugin-3.17.5.jar"> + <sha256 value="f92605eca13dc00ad8cb3c0d0317bdb7ad410bbce4cdfebf11a451f6a65a7040" origin="Generated by Gradle"/> + </artifact> + <artifact name="develocity-gradle-plugin-3.17.5.module"> + <sha256 value="6a7182b1f298153cf2efdec39f3ecd3014624f76d4872a78b180c1f0ea363484" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.gradle.develocity" name="com.gradle.develocity.gradle.plugin" version="3.17.4"> + <artifact name="com.gradle.develocity.gradle.plugin-3.17.4.pom"> + <sha256 value="e0032ae0d8d747734cbc55f81f49484ada02698067c5dda08a59ec2a7c6395b2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.gradle.develocity" name="com.gradle.develocity.gradle.plugin" version="3.17.5"> + <artifact name="com.gradle.develocity.gradle.plugin-3.17.5.pom"> + <sha256 value="eaa04ccc3e8bdc4f96e53737e529e914fee8892b0a1c3d3e5162d47549fa58a1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.ibm.icu" name="icu4j" version="74.2"> + <artifact name="icu4j-74.2.jar"> + <sha256 value="95c055080e14c093ebeeba5b733e1a1be7a4af5854668c774cedf070d4240e43" origin="Generated by Gradle"/> + </artifact> + <artifact name="icu4j-74.2.pom"> + <sha256 value="ad5c4681cffa8362fa63462515586e0a39492e86eeb3f2d8d8db8d527585463e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.ibm.icu" name="icu4j-root" version="74.2"> + <artifact name="icu4j-root-74.2.pom"> + <sha256 value="a0daf2ab7b3d63b093ae76dd2fd9858df27508cee3b29f00b7dd6d25a1fde9d6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.intellij" name="annotations" version="12.0"> + <artifact name="annotations-12.0.jar"> + <sha256 value="f8ab13b14be080fe2f617f90e55599760e4a1b4deeea5c595df63d0d6375ed6d" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotations-12.0.pom"> + <sha256 value="faf82de0dc02e0c0ae327cd653f37255496b2e53fce280b3ab4cb34553a89086" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.mikepenz" name="fastadapter" version="5.5.1"> + <artifact name="fastadapter-5.5.1.aar"> + <sha256 value="dcb0efcf3bd0a231c4ae7705b8db13fbf4d95046ecaebc05f94b8f20d84fe69d" origin="Generated by Gradle"/> + </artifact> + <artifact name="fastadapter-5.5.1.module"> + <sha256 value="117e399cf545f3199bb536bb60a1f816cba55a4ad05440d60dbdffdb5bf4572d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.mikepenz" name="fastadapter-extensions-expandable" version="5.5.1"> + <artifact name="fastadapter-extensions-expandable-5.5.1.aar"> + <sha256 value="833755911f4721e0c8fd8644211e178df00256e7e179ed71b9a2a2811fd7b734" origin="Generated by Gradle"/> + </artifact> + <artifact name="fastadapter-extensions-expandable-5.5.1.module"> + <sha256 value="01b3ced001e936a98864e7c76fa7c845bdc90af1e3cbb0e6d96d6976552f36df" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.mikepenz" name="google-material-typeface" version="4.0.0.3-kotlin"> + <artifact name="google-material-typeface-4.0.0.3-kotlin-sources.jar"> + <sha256 value="a91846450b6dc24b835c5cdbee71905f57fc9ba0eb44627ebce081d20842a1e1" origin="Generated by Gradle"/> + </artifact> + <artifact name="google-material-typeface-4.0.0.3-kotlin.aar"> + <sha256 value="a2191656322598a5a2ccde0aa715afefdef3dc23291b71a76eebd7981692abb9" origin="Generated by Gradle"/> + </artifact> + <artifact name="google-material-typeface-4.0.0.3-kotlin.module"> + <sha256 value="d1c0cef8899de0de09bcf14fc366f81493411362ecb646a23c42b021fa6660fc" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.mikepenz" name="iconics-core" version="5.3.2"> + <artifact name="iconics-core-5.3.2.aar"> + <sha256 value="dd59f711fc024015d5831a4672fd49d32ca67b5a3e9e7fa5bf9d881e780dd36e" origin="Generated by Gradle"/> + </artifact> + <artifact name="iconics-core-5.3.2.module"> + <sha256 value="19b08bd366faf91237209e498cf294079501f7f71c7b1720cd12deee04afc5aa" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.mikepenz" name="iconics-typeface-api" version="5.3.2"> + <artifact name="iconics-typeface-api-5.3.2.aar"> + <sha256 value="2c492dba56b8d3cad3527f46175e7e5967bde99207afd7556d18755e46556ad8" origin="Generated by Gradle"/> + </artifact> + <artifact name="iconics-typeface-api-5.3.2.module"> + <sha256 value="f7aca8af9f3257ec0fdadddc854b537f5c970bdeab5e8a6d4db27075bca8b0f1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.mikepenz" name="iconics-typeface-api" version="5.5.0-b01"> + <artifact name="iconics-typeface-api-5.5.0-b01.aar"> + <sha256 value="258b2b1daee3fc1ea3d114ae48fb2005c8e58f5b6e61c1d91daf0c2ce654fc0d" origin="Generated by Gradle"/> + </artifact> + <artifact name="iconics-typeface-api-5.5.0-b01.module"> + <sha256 value="0b41a59343da6bb57616b9849652d7a04d8afb8deae1b1c8175b83461985c7e5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.mikepenz" name="materialdrawer" version="8.4.5"> + <artifact name="materialdrawer-8.4.5.aar"> + <sha256 value="f54a12caa991e23eccb284a2bcd9fb33d5782d8c5402b771b9f17e23675e4730" origin="Generated by Gradle"/> + </artifact> + <artifact name="materialdrawer-8.4.5.module"> + <sha256 value="05d34eb0f3ba784b985a6d205c9952a1f4afc19c1ec16a9bdaa34066b10b94d6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.mikepenz" name="materialdrawer-iconics" version="8.4.5"> + <artifact name="materialdrawer-iconics-8.4.5.aar"> + <sha256 value="4619cd9ba3df5a51241d5b50c227378b0512d591107330f0f8a20d6c5e86492a" origin="Generated by Gradle"/> + </artifact> + <artifact name="materialdrawer-iconics-8.4.5.module"> + <sha256 value="824509161c2fc38f0ff06b58ead89a36c3af73ddfe7ac514cbfa1d0f726003df" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.pinterest.ktlint" name="ktlint-cli" version="1.0.1"> + <artifact name="ktlint-cli-1.0.1.jar"> + <sha256 value="81889a9cab5da042aea48869b170299fec7d6f66773aa7d5245b8b8ccb8d0c89" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-cli-1.0.1.module"> + <sha256 value="1013cec46eb04dba1f3aa4ac447b98d214866677b1bef672328bf077ff0d7755" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.pinterest.ktlint" name="ktlint-cli-reporter-baseline" version="1.0.1"> + <artifact name="ktlint-cli-reporter-baseline-1.0.1.jar"> + <sha256 value="2e7f0c581540a7674b05d36735bfcabe84b14b47de10f13f7f3738cc476f0bd4" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-cli-reporter-baseline-1.0.1.module"> + <sha256 value="5a357f0a878f6bacad7e009aa446ae53cf72efbb795944009442d233bf75b6b2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.pinterest.ktlint" name="ktlint-cli-reporter-checkstyle" version="1.0.1"> + <artifact name="ktlint-cli-reporter-checkstyle-1.0.1.jar"> + <sha256 value="fb41b8b1f46987cceaac03b1ae526e132d284fd24c2573167369af7f3a672775" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-cli-reporter-checkstyle-1.0.1.module"> + <sha256 value="39aeb181903e8fafd651c87e2683a8d885c0b38c448b4dab86d4b6aece8beab8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.pinterest.ktlint" name="ktlint-cli-reporter-core" version="1.0.1"> + <artifact name="ktlint-cli-reporter-core-1.0.1.jar"> + <sha256 value="4e7319d32bd1d990fe0dae234582908d08e8514aef84e1dad83cb9a5a159ade3" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-cli-reporter-core-1.0.1.module"> + <sha256 value="e466ea107dfe367b6da7bbe86b0ea856791bf1b5574e4c361b126a56d71de8b3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.pinterest.ktlint" name="ktlint-cli-reporter-format" version="1.0.1"> + <artifact name="ktlint-cli-reporter-format-1.0.1.jar"> + <sha256 value="bd2e23533c2f9d625b7b2519439385802d80fc0764620c8bdc42a49e93370b64" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-cli-reporter-format-1.0.1.module"> + <sha256 value="fe66e4e160a2adbce020017c8e3201e2fcc0f9286f5642f4e4af222304766d89" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.pinterest.ktlint" name="ktlint-cli-reporter-html" version="1.0.1"> + <artifact name="ktlint-cli-reporter-html-1.0.1.jar"> + <sha256 value="7243022af49f92d4ef495aee161adf65f6c4639997952980acf237988754e964" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-cli-reporter-html-1.0.1.module"> + <sha256 value="d59d01331d723cc44e70b1ca5ed72275bf3d278789559b8fe7bb9d6e05b6cb7a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.pinterest.ktlint" name="ktlint-cli-reporter-json" version="1.0.1"> + <artifact name="ktlint-cli-reporter-json-1.0.1.jar"> + <sha256 value="4ec9975b68dd950b41d15db343602fff448c858bac2e3643f85cdf5de2525bee" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-cli-reporter-json-1.0.1.module"> + <sha256 value="825fe7ec674cff0c3a7b26bed198b986595467ba8b5e5dae90cfc43c2c9b46bd" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.pinterest.ktlint" name="ktlint-cli-reporter-plain" version="1.0.1"> + <artifact name="ktlint-cli-reporter-plain-1.0.1.jar"> + <sha256 value="ab211bb3b9076f8cf4536fca39e757a7518fd2864151407cb025f0564bc59892" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-cli-reporter-plain-1.0.1.module"> + <sha256 value="6856c13679b833b95afdd77a76f68a4c35331edd53332303dafa4465ff219fe5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.pinterest.ktlint" name="ktlint-cli-reporter-plain-summary" version="1.0.1"> + <artifact name="ktlint-cli-reporter-plain-summary-1.0.1.jar"> + <sha256 value="404f6ea0c2b6fd6f19eb8a488a44b7fc0b4f8e116dccba2ea921c506fc4604be" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-cli-reporter-plain-summary-1.0.1.module"> + <sha256 value="580fcd1d4048928cd9230f6f2d52476c5ed794997589ec771fe5b0d34a8fe40d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.pinterest.ktlint" name="ktlint-cli-reporter-sarif" version="1.0.1"> + <artifact name="ktlint-cli-reporter-sarif-1.0.1.jar"> + <sha256 value="31e170b8ce281c8ac192f42fbd1efc521b9250c13ed67a5898b72c54d390c7b0" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-cli-reporter-sarif-1.0.1.module"> + <sha256 value="7b02755b5d4c103c0e29f787a2910558ba943a371451265ba1b061bd564238d4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.pinterest.ktlint" name="ktlint-cli-ruleset-core" version="1.0.1"> + <artifact name="ktlint-cli-ruleset-core-1.0.1.jar"> + <sha256 value="218c602960994903a672d7a191dd01b6fd015a86b67145a470bb57deeb388f5a" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-cli-ruleset-core-1.0.1.module"> + <sha256 value="25c84bf9e92d259b5077c27851bdd92ad4b8dd61227054c535f16c20edc20f54" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.pinterest.ktlint" name="ktlint-logger" version="1.0.1"> + <artifact name="ktlint-logger-1.0.1.jar"> + <sha256 value="4d11c82204f55940b6354a26c9a71bc3586f27528bcb294661b87964943c526f" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-logger-1.0.1.module"> + <sha256 value="7041ce959fbfb388da63753cd1833b2fcb93b6ddc5c03a268e5aa807a6e6c37f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.pinterest.ktlint" name="ktlint-rule-engine" version="1.0.1"> + <artifact name="ktlint-rule-engine-1.0.1.jar"> + <sha256 value="38ea20553969a554ff2293b2c638869cf0dfef175edeed69ade8ff749b8a09dc" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-rule-engine-1.0.1.module"> + <sha256 value="9168ba81f8d9eb855e09053c3133e1925323e7685317352a7f03c0677f12e3da" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.pinterest.ktlint" name="ktlint-rule-engine-core" version="1.0.1"> + <artifact name="ktlint-rule-engine-core-1.0.1.jar"> + <sha256 value="ee4417d9a9fe1d42c700b90acb68f0afb3bd5ab8a4c6cfa5d3733887631d654e" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-rule-engine-core-1.0.1.module"> + <sha256 value="dae1a5b2c1efb042305e5b29879ba4181e2c14d8c5cb2a704a021d7a8f6ad4fe" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.pinterest.ktlint" name="ktlint-ruleset-standard" version="1.0.1"> + <artifact name="ktlint-ruleset-standard-1.0.1.jar"> + <sha256 value="a19d94b7dc967d7433ff9e486037f9db7cfc6ba7739f62f9049f86f3a2fb0273" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-ruleset-standard-1.0.1.module"> + <sha256 value="fd6fc869b1a0df0a45d391fd0e768b0771a7e8d376da8e1dc801afe02aac2815" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup" name="javapoet" version="1.10.0"> + <artifact name="javapoet-1.10.0.pom"> + <sha256 value="1690340a222279f2cbadf373e88826fa20f7f3cc3ec0252f36818fed32701ab1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup" name="javapoet" version="1.13.0"> + <artifact name="javapoet-1.13.0.jar"> + <sha256 value="4c7517e848a71b36d069d12bb3bf46a70fd4cda3105d822b0ed2e19c00b69291" origin="Generated by Gradle"/> + </artifact> + <artifact name="javapoet-1.13.0.pom"> + <sha256 value="54a34fa8502a46bc90efdb49262600591fa80bf9a34f5a4c798311aec16ca977" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup" name="javawriter" version="2.1.1"> + <artifact name="javawriter-2.1.1-sources.jar"> + <sha256 value="f57646206c5908c69342022c7b5ccbc15dbf605ce80421da3b96af9f86331e28" origin="Generated by Gradle"/> + </artifact> + <artifact name="javawriter-2.1.1.jar"> + <sha256 value="f699823d0081f69cbb676c1845ea222e0ada79bc88a53e5d22d8bd02d328f57e" origin="Generated by Gradle"/> + </artifact> + <artifact name="javawriter-2.1.1.pom"> + <sha256 value="d47fc646324c22c66f2b0e0e743c850dde9a51990c53925e7501d960f2e8df84" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup" name="javawriter" version="2.5.0"> + <artifact name="javawriter-2.5.0.jar"> + <sha256 value="fcfb09fb0ea0aa97d3cfe7ea792398081348e468f126b3603cb3803f240197f0" origin="Generated by Gradle"/> + </artifact> + <artifact name="javawriter-2.5.0.pom"> + <sha256 value="e1abd7f1116cf5e0c59947693e2189208ec94296b2a3394c959e3511d399a7b0" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup" name="kotlinpoet" version="1.14.2"> + <artifact name="kotlinpoet-1.14.2.jar"> + <sha256 value="102d5d8a289d961cd7f39204c264d272e4aad775e388d909f6050e14558aae9b" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinpoet-1.14.2.module"> + <sha256 value="4eaf793bea0717db41a4b16a53d34fea2ed2c8c78d3625772c979d431a112e59" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup" name="kotlinpoet-javapoet" version="1.14.2"> + <artifact name="kotlinpoet-javapoet-1.14.2.jar"> + <sha256 value="892aa78f3a3df30daaec099d06ef0b8e884e96111a9e14961cbf745aff371f1b" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinpoet-javapoet-1.14.2.module"> + <sha256 value="013acffeeee7c5b2edc73cd8a393a8d2bddcfde4ccd4b23d32f1de12c3df9372" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup" name="kotlinpoet-ksp" version="1.13.2"> + <artifact name="kotlinpoet-ksp-1.13.2.jar"> + <sha256 value="d1de5e3d6f7405dc10cb14a43f39400ba75604e1cf86da1fe08e9007a8a17d08" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinpoet-ksp-1.13.2.module"> + <sha256 value="9e08d4f1957d630c380a291735a2fde6de5324b0906c640742f0ea5368eb5b08" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup.moshi" name="moshi" version="1.15.1"> + <artifact name="moshi-1.15.1.jar"> + <sha256 value="46a1118fe1fc12723a575c94133fc8936dcc78d3f8873c0e70a055de9e5861a6" origin="Generated by Gradle"/> + </artifact> + <artifact name="moshi-1.15.1.module"> + <sha256 value="671f5355aba7d9fea64be14e371fc295c34dabf872d210924bf0db4291d6ef74" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup.moshi" name="moshi-adapters" version="1.15.1"> + <artifact name="moshi-adapters-1.15.1.jar"> + <sha256 value="923f9fbc750db262877e9e5b3ae192c80c5b8fbeb1cc196885860a6e8adc4c3e" origin="Generated by Gradle"/> + </artifact> + <artifact name="moshi-adapters-1.15.1.module"> + <sha256 value="116a972453a8fa39cfc1fb8bab5e8396d4cd9036f1347148f0f4104183c58cd1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup.moshi" name="moshi-kotlin-codegen" version="1.15.1"> + <artifact name="moshi-kotlin-codegen-1.15.1.jar"> + <sha256 value="14ab79736dc806a245ad76354eb7d53e83bead1d87b092a6b07da6a3287a39ed" origin="Generated by Gradle"/> + </artifact> + <artifact name="moshi-kotlin-codegen-1.15.1.module"> + <sha256 value="520bd9f9a92040b1431c330acd2395aceba925028b720cd3e54bb58c61fae178" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup.okhttp3" name="logging-interceptor" version="4.12.0"> + <artifact name="logging-interceptor-4.12.0-sources.jar"> + <sha256 value="967335783f8af3fca7819f9f343f753243f2877c5480099e2084fe493af7da82" origin="Generated by Gradle"/> + </artifact> + <artifact name="logging-interceptor-4.12.0.jar"> + <sha256 value="f3e8d5f0903c250c2b55d2f47fcfe008e80634385da8385161c7a63aaed0c74c" origin="Generated by Gradle"/> + </artifact> + <artifact name="logging-interceptor-4.12.0.module"> + <sha256 value="2e2148cd213b4f8f54d66d0548a228057e1f7b67332e0f979b1a1ce36b2276a7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup.okhttp3" name="mockwebserver" version="4.12.0"> + <artifact name="mockwebserver-4.12.0-sources.jar"> + <sha256 value="99d5c408b6da5d40d00694136d875007dc336d78e954ddedd39201e1cc7fb311" origin="Generated by Gradle"/> + </artifact> + <artifact name="mockwebserver-4.12.0.jar"> + <sha256 value="6784673687f4ac8f21679b9d4bc7cdb46e1a1ce1be9d3133b36bede59a741561" origin="Generated by Gradle"/> + </artifact> + <artifact name="mockwebserver-4.12.0.module"> + <sha256 value="93e4696552118b6425462aed936edfd320c9fcedef0a82d5741c0a7d53c0cf74" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup.okhttp3" name="okhttp" version="4.12.0"> + <artifact name="okhttp-4.12.0-sources.jar"> + <sha256 value="d91a769a4140e542cddbac4e67fcf279299614e8bfd53bd23b85e60c2861341c" origin="Generated by Gradle"/> + </artifact> + <artifact name="okhttp-4.12.0.jar"> + <sha256 value="b1050081b14bb7a3a7e55a4d3ef01b5dcfabc453b4573a4fc019767191d5f4e0" origin="Generated by Gradle"/> + </artifact> + <artifact name="okhttp-4.12.0.module"> + <sha256 value="607e220ff8215b929d829bbf54f332894f1459b4d795979aeafcbcc1cea54cf3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup.okio" name="okio" version="3.6.0"> + <artifact name="okio-3.6.0.module"> + <sha256 value="6a47ac50364e6598459401fb86f9b6cfcdf637b9b3a3045b1cc33cbf4c408218" origin="Generated by Gradle"/> + </artifact> + <artifact name="okio-metadata-3.6.0-all.jar"> + <sha256 value="2bbd3f0645a3ada7e6532b2e6db471af4861464e1a140f95f807dfd16aa049e3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup.okio" name="okio" version="3.7.0"> + <artifact name="okio-3.7.0.module"> + <sha256 value="f3cae009f0b6c842fbbc52ce77542c19d19d56ee99a5e555647f0bafc9d50cfa" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup.okio" name="okio" version="3.9.0"> + <artifact name="okio-3.9.0.module"> + <sha256 value="68d1c879ff658931d02b3adbeafbb512e163c20a90cadf07d90c8dbea7e76210" origin="Generated by Gradle"/> + </artifact> + <artifact name="okio-metadata-3.9.0.jar"> + <sha256 value="e518a59856273a1fce18ac7d13ddfc690defa6122d7e5cd00cae19b62d0347d9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup.okio" name="okio-jvm" version="3.7.0"> + <artifact name="okio-jvm-3.7.0.jar"> + <sha256 value="d8b35adc28768f43ae5afe6a7d1aa2a878ba51e0b96a4f308811f3b1f5b13e55" origin="Generated by Gradle"/> + </artifact> + <artifact name="okio-jvm-3.7.0.module"> + <sha256 value="6fae0201b0ae48a19606de006ffe9842d8d0fc2a1e434e0785cecd8b75abe474" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup.okio" name="okio-jvm" version="3.9.0"> + <artifact name="okio-jvm-3.9.0-sources.jar"> + <sha256 value="b8ab886c9ed94b6d22fe177efab23f66b2fe0cbcfbf9902d226667038410e0b1" origin="Generated by Gradle"/> + </artifact> + <artifact name="okio-jvm-3.9.0.jar"> + <sha256 value="ddc386ff14bd25d5c934167196eaf45b18de4f28e1c55a4db37ae594cbfd37e4" origin="Generated by Gradle"/> + </artifact> + <artifact name="okio-jvm-3.9.0.module"> + <sha256 value="cf97284ec61bb51e6dfcbc7f2b6d9556c9b7b3e3cb22cc0e2d4f0eefbf36195b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup.retrofit2" name="converter-moshi" version="2.11.0"> + <artifact name="converter-moshi-2.11.0-sources.jar"> + <sha256 value="41b6c659c4401cbbf63d0787b78a13d52848b29f71ae9f4c69bfc9641cd88f47" origin="Generated by Gradle"/> + </artifact> + <artifact name="converter-moshi-2.11.0.jar"> + <sha256 value="dbfae9dbde0b61e5c5671648c8d9fd486efae9ad07d573ed01bd0de4688d9b2c" origin="Generated by Gradle"/> + </artifact> + <artifact name="converter-moshi-2.11.0.module"> + <sha256 value="f2397c76f40fb873d4c9d1af830b44a25b72ef9829b026099e2913078b8df33e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.squareup.retrofit2" name="retrofit" version="2.11.0"> + <artifact name="retrofit-2.11.0.jar"> + <sha256 value="9f4fbbce70728584fbeed38d4061f36d4477e89bca74b4e2ac8aeb6819b0fe43" origin="Generated by Gradle"/> + </artifact> + <artifact name="retrofit-2.11.0.module"> + <sha256 value="e9bb2e201844c88e4750624f9114a6354b51b70948b44d5c6b4a94e8cd070a81" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.sun.activation" name="all" version="1.2.0"> + <artifact name="all-1.2.0.pom"> + <sha256 value="1d8518e3ac7532a104e4f7be77def37c982e530723c6bdb3d67708cce2b0c2c4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.sun.activation" name="all" version="1.2.1"> + <artifact name="all-1.2.1.pom"> + <sha256 value="360883bf64486ecef161b8f282f6503536dd1a670d53a0a871c8fb20170e6795" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.sun.activation" name="javax.activation" version="1.2.0"> + <artifact name="javax.activation-1.2.0.jar"> + <sha256 value="993302b16cd7056f21e779cc577d175a810bb4900ef73cd8fbf2b50f928ba9ce" origin="Generated by Gradle"/> + </artifact> + <artifact name="javax.activation-1.2.0.pom"> + <sha256 value="f879b6e945854c6900b0dbee1c8384d7ab3de7e157fd7ac84937405c416d2a5e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.sun.istack" name="istack-commons" version="3.0.8"> + <artifact name="istack-commons-3.0.8.pom"> + <sha256 value="a0f0517e8512f0fbcc7b8295c12f6566a3d0c2d86d655639dc662ef8c0c7ebe5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.sun.istack" name="istack-commons-runtime" version="3.0.8"> + <artifact name="istack-commons-runtime-3.0.8.jar"> + <sha256 value="4ffabb06be454a05e4398e20c77fa2b6308d4b88dfbef7ca30a76b5b7d5505ef" origin="Generated by Gradle"/> + </artifact> + <artifact name="istack-commons-runtime-3.0.8.pom"> + <sha256 value="c2e014d34cb84ed287d064986c45c305a4124228a7337eccf6c421d14d708f1c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.sun.xml.bind" name="jaxb-bom-ext" version="2.3.2"> + <artifact name="jaxb-bom-ext-2.3.2.pom"> + <sha256 value="1a7dec2b27e7e055744cdb8cf1b90def4fd473acd1b804eff098139358959bd7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.sun.xml.bind.mvn" name="jaxb-parent" version="2.3.2"> + <artifact name="jaxb-parent-2.3.2.pom"> + <sha256 value="20dd6dc34ab7549ac40da1d82e92222ec4347ad0ec0cb118ef6c5703bed53a18" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.sun.xml.bind.mvn" name="jaxb-runtime-parent" version="2.3.2"> + <artifact name="jaxb-runtime-parent-2.3.2.pom"> + <sha256 value="b24f8d51f184a68bc1b86d48c0e3cfefeb21a44ede1c5f7303c58ae0488533ec" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.sun.xml.bind.mvn" name="jaxb-txw-parent" version="2.3.2"> + <artifact name="jaxb-txw-parent-2.3.2.pom"> + <sha256 value="b55d3efa9b158f483a30e92c78ccb600f93314733d089eba9b74436f01b314a4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.sun.xml.fastinfoset" name="FastInfoset" version="1.2.16"> + <artifact name="FastInfoset-1.2.16.jar"> + <sha256 value="056f3a1e144409f21ed16afc26805f58e9a21f3fce1543c42d400719d250c511" origin="Generated by Gradle"/> + </artifact> + <artifact name="FastInfoset-1.2.16.pom"> + <sha256 value="e147d258ab6e6691f70599a952400e6e6c7558f8c9c028dbe1be23178308e830" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="com.sun.xml.fastinfoset" name="fastinfoset-project" version="1.2.16"> + <artifact name="fastinfoset-project-1.2.16.pom"> + <sha256 value="90582425adc1f40b41362dafb95173931225acaa5e79620d5e6bd52f646292ba" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="commons-codec" name="commons-codec" version="1.10"> + <artifact name="commons-codec-1.10.jar"> + <sha256 value="4241dfa94e711d435f29a4604a3e2de5c4aa3c165e23bd066be6fc1fc4309569" origin="Generated by Gradle"/> + </artifact> + <artifact name="commons-codec-1.10.pom"> + <sha256 value="bdb8db7012d112a6e3ea8fdb7c510b300d99eff0819d27dddba9c43397ea4cfb" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="commons-codec" name="commons-codec" version="1.11"> + <artifact name="commons-codec-1.11.jar"> + <sha256 value="e599d5318e97aa48f42136a2927e6dfa4e8881dff0e6c8e3109ddbbff51d7b7d" origin="Generated by Gradle"/> + </artifact> + <artifact name="commons-codec-1.11.pom"> + <sha256 value="c1e7140d1dea8fdf3528bc1e3c5444ac0b541297311f45f9806c213ec3ee9a10" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="commons-codec" name="commons-codec" version="1.15"> + <artifact name="commons-codec-1.15.jar"> + <sha256 value="b3e9f6d63a790109bf0d056611fbed1cf69055826defeb9894a71369d246ed63" origin="Generated by Gradle"/> + </artifact> + <artifact name="commons-codec-1.15.pom"> + <sha256 value="c86ee198a35a3715487860f419cbf642e7e4d9e8714777947dbe6a4e3a20ab58" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="commons-io" name="commons-io" version="2.13.0"> + <artifact name="commons-io-2.13.0.jar"> + <sha256 value="671eaa39688dac2ffaa4645b3c9980ae2d0ea2471e4ae6a5da199cd15ae23666" origin="Generated by Gradle"/> + </artifact> + <artifact name="commons-io-2.13.0.pom"> + <sha256 value="db3fed64c2e1774ebfd6b1a749037732b149b9111dd7e6b985f08dda55470439" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="commons-logging" name="commons-logging" version="1.2"> + <artifact name="commons-logging-1.2.jar"> + <sha256 value="daddea1ea0be0f56978ab3006b8ac92834afeefbd9b7e4e6316fca57df0fa636" origin="Generated by Gradle"/> + </artifact> + <artifact name="commons-logging-1.2.pom"> + <sha256 value="c91ab5aa570d86f6fd07cc158ec6bc2c50080402972ee9179fe24100739fbb20" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="de.c1710" name="filemojicompat" version="3.2.7"> + <artifact name="filemojicompat-3.2.7.aar"> + <sha256 value="490962a1a195a2c8a40e1d16649e22b56acfc6bc09388f565dbfacfed36851be" origin="Generated by Gradle"/> + </artifact> + <artifact name="filemojicompat-3.2.7.module"> + <sha256 value="7c10f1aa937271d5f9e8ce1b26e21f1ab7f7c07eb89a7cf09caac851eae2d111" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="de.c1710" name="filemojicompat-defaults" version="3.2.7"> + <artifact name="filemojicompat-defaults-3.2.7.aar"> + <sha256 value="9e88f2479567f31eab6688066d1cf2c225537a3fde187638bcb35f163129c7d1" origin="Generated by Gradle"/> + </artifact> + <artifact name="filemojicompat-defaults-3.2.7.module"> + <sha256 value="af601eb6b31e7f0527de638a166c894208321a9acdc79ecc06d16fcc44c22074" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="de.c1710" name="filemojicompat-ui" version="3.2.7"> + <artifact name="filemojicompat-ui-3.2.7.aar"> + <sha256 value="6f00896b039e4ab8cd9b7c68eda76f45bbffeeac3f4f14c0671336844a075082" origin="Generated by Gradle"/> + </artifact> + <artifact name="filemojicompat-ui-3.2.7.module"> + <sha256 value="ca4cc1000d477aec96203dad26b1982e0b22dab59608f58c2f418aa3cd64bd7a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="dev.drewhamilton.poko" name="poko-annotations" version="0.15.0"> + <artifact name="poko-annotations-0.15.0.module"> + <sha256 value="66d7224d73cd1c350b7523e2d70c11e366fbea51d9cf61ff1099149de35d7fb0" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="dev.drewhamilton.poko" name="poko-annotations-jvm" version="0.15.0"> + <artifact name="poko-annotations-jvm-0.15.0-sources.jar"> + <sha256 value="7a735dde85bad254b3b48a1e272de91640c80cf855fafbeee569ba66edeced6d" origin="Generated by Gradle"/> + </artifact> + <artifact name="poko-annotations-jvm-0.15.0.jar"> + <sha256 value="3bcb96770ac52075c431201e29fd5e67924435555df22f5e5ab2602b7b773635" origin="Generated by Gradle"/> + </artifact> + <artifact name="poko-annotations-jvm-0.15.0.module"> + <sha256 value="b79c2f6f898fa8e1bf1415a85a14f98fd99f770e00b3e91e9ba6cf1d915bbbff" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="info.picocli" name="picocli" version="4.7.5"> + <artifact name="picocli-4.7.5.jar"> + <sha256 value="e83a906fb99b57091d1d68ac11f7c3d2518bd7a81a9c71b259e2c00d1564c8e8" origin="Generated by Gradle"/> + </artifact> + <artifact name="picocli-4.7.5.pom"> + <sha256 value="7e4e8b0f4b7ca607520b20d2f1e58df4f934ca636cb1eba8289cff2f2956bfd8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.github.detekt.sarif4k" name="sarif4k" version="0.5.0"> + <artifact name="sarif4k-0.5.0.module"> + <sha256 value="e72a62f07b569d13a5d66ba837e74ac33c68cfef01c13f3adde183e7ae64ce89" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.github.detekt.sarif4k" name="sarif4k-jvm" version="0.5.0"> + <artifact name="sarif4k-jvm-0.5.0-sources.jar"> + <sha256 value="e8c4b95152fbc5ffcde24bdaf55b69dad95394cda2b47cbad6bd11c8dfdfe5c9" origin="Generated by Gradle"/> + </artifact> + <artifact name="sarif4k-jvm-0.5.0.jar"> + <sha256 value="ead041938323b34336138cf43e1875d8aff90b5e7e9411dbede3d201a327f1a7" origin="Generated by Gradle"/> + </artifact> + <artifact name="sarif4k-jvm-0.5.0.module"> + <sha256 value="948a3bad5ce43582aad460366096ca50b491f68a86b586189deb3fa8350e76b1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.github.oshai" name="kotlin-logging" version="5.1.0"> + <artifact name="kotlin-logging-5.1.0.module"> + <sha256 value="dd3ab77e0ec3508ca08980b5426eda6623ce8a5b7d422f383da1607b8513e384" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.github.oshai" name="kotlin-logging-jvm" version="5.1.0"> + <artifact name="kotlin-logging-jvm-5.1.0-sources.jar"> + <sha256 value="88404db4ab59b6d234c3828f5605c3cc47ddd834b8b85ebce016eda32a043142" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-logging-jvm-5.1.0.jar"> + <sha256 value="0e918ecb22a61b99f1eb9acebd689fdaa9a76dc9fe972c360fd36299a92aa892" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-logging-jvm-5.1.0.module"> + <sha256 value="23991be08eafe5c7b97c0f211d0bf7f0cbfd4276d8630e874cc61f6930375dc6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.grpc" name="grpc-api" version="1.57.0"> + <artifact name="grpc-api-1.57.0.jar"> + <sha256 value="8d2c384299f84ee8aa7f670f00e7cb26b87e231cf3091474307b32b76910f71c" origin="Generated by Gradle"/> + </artifact> + <artifact name="grpc-api-1.57.0.pom"> + <sha256 value="c3f054a7c8861647d0a55825b0a949f44fcfc9c64f479083d96813814c502612" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.grpc" name="grpc-context" version="1.57.0"> + <artifact name="grpc-context-1.57.0.jar"> + <sha256 value="953fcacd82f531e69b76e3834f5830bad4c22ae84144e058d71dc80a7430275d" origin="Generated by Gradle"/> + </artifact> + <artifact name="grpc-context-1.57.0.pom"> + <sha256 value="ab264e82bfb6ab895f6016af8b3cc44495abc81e769c30fa5a9ae0ad16be6cc6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.grpc" name="grpc-core" version="1.57.0"> + <artifact name="grpc-core-1.57.0.jar"> + <sha256 value="3bee48c73bc4c5b55bed79be0e484adf26ba56bebbe5798ddbf34714ef1e1cea" origin="Generated by Gradle"/> + </artifact> + <artifact name="grpc-core-1.57.0.pom"> + <sha256 value="8184045f5791e00cf2cdbcf5e8846afd48f5cf7e5a4f38fb5b8303c7b2efe55b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.grpc" name="grpc-netty" version="1.57.0"> + <artifact name="grpc-netty-1.57.0.jar"> + <sha256 value="81d43f2d4ed18fa341bd840a3735f1403a70074a046e157e27f679b721b4c9ad" origin="Generated by Gradle"/> + </artifact> + <artifact name="grpc-netty-1.57.0.pom"> + <sha256 value="ed9dfdd7b1ed4356afb3c5d1407dedb634c8602fb410479b480ad8c1139ee17b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.grpc" name="grpc-protobuf" version="1.57.0"> + <artifact name="grpc-protobuf-1.57.0.jar"> + <sha256 value="49f986d4eab12610fdba4a6890fca52d5eb653598916fdb863a366d5e28eecf7" origin="Generated by Gradle"/> + </artifact> + <artifact name="grpc-protobuf-1.57.0.pom"> + <sha256 value="c0dcb8c67fd01daa63256f0f8b68d3707ceb7ca85cdaab7a3c6c3ff460e13dd1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.grpc" name="grpc-protobuf-lite" version="1.57.0"> + <artifact name="grpc-protobuf-lite-1.57.0.jar"> + <sha256 value="2c507c02d981b84a21763d44e09af4f279881dd3e25be3080f6361258607f198" origin="Generated by Gradle"/> + </artifact> + <artifact name="grpc-protobuf-lite-1.57.0.pom"> + <sha256 value="b023be7008849489f652eeff75fd0fe1aa9c905f671d344e16daafddf921819d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.grpc" name="grpc-stub" version="1.57.0"> + <artifact name="grpc-stub-1.57.0.jar"> + <sha256 value="6e6ee141539fa14d9fa479f7f511605544443c7e011e78e273cf9468aa183060" origin="Generated by Gradle"/> + </artifact> + <artifact name="grpc-stub-1.57.0.pom"> + <sha256 value="6d4459487c621dff31510a88829063631e915d2dcc774d71928418116e59a88d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.netty" name="netty-buffer" version="4.1.93.Final"> + <artifact name="netty-buffer-4.1.93.Final.jar"> + <sha256 value="007c7d9c378df02d390567d0d7ddf542ffddb021b7313dbf502392113ffabb08" origin="Generated by Gradle"/> + </artifact> + <artifact name="netty-buffer-4.1.93.Final.pom"> + <sha256 value="83fbc54e2b73b86d55b208f618d1a2a156910ec146f7ece57565007b750add78" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.netty" name="netty-codec" version="4.1.93.Final"> + <artifact name="netty-codec-4.1.93.Final.jar"> + <sha256 value="990c378168dc6364c6ff569701f4f2f122fffe8998b3e189eba4c4d868ed1084" origin="Generated by Gradle"/> + </artifact> + <artifact name="netty-codec-4.1.93.Final.pom"> + <sha256 value="19cded267a070dff1abc9d029b552fad262acc1aba5c6c67b2278fca113a26ab" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.netty" name="netty-codec-http" version="4.1.93.Final"> + <artifact name="netty-codec-http-4.1.93.Final.jar"> + <sha256 value="dacf78ce78ab2d29570325db4cd2451ea589639807de95881a0fa7155a9e6b55" origin="Generated by Gradle"/> + </artifact> + <artifact name="netty-codec-http-4.1.93.Final.pom"> + <sha256 value="a3dafff071b6d284e8063d96843de2bb83cf3b889eae0cc9e0adb64a57a31b82" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.netty" name="netty-codec-http2" version="4.1.93.Final"> + <artifact name="netty-codec-http2-4.1.93.Final.jar"> + <sha256 value="d96cc09045a1341c6d47494352aa263b87b72fb1d2ea9eca161aa73820bfe8bb" origin="Generated by Gradle"/> + </artifact> + <artifact name="netty-codec-http2-4.1.93.Final.pom"> + <sha256 value="084433b42d541f7ac4b59287dd253285cfda3a3d65de72e7368bb7ec3d367271" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.netty" name="netty-codec-socks" version="4.1.93.Final"> + <artifact name="netty-codec-socks-4.1.93.Final.jar"> + <sha256 value="0ea47b5ba23ca1da8eb9146c8fc755c1271414633b1e2be2ce1df764ba0fff2a" origin="Generated by Gradle"/> + </artifact> + <artifact name="netty-codec-socks-4.1.93.Final.pom"> + <sha256 value="8cd816ed991a946041bab4cb24bd9c8e41ee0692512511f2d83cef538eb605d7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.netty" name="netty-common" version="4.1.93.Final"> + <artifact name="netty-common-4.1.93.Final.jar"> + <sha256 value="443bb316599fb16e3baeba2fb58881814d7ff0b7af176fe76e38071a6e86f8c0" origin="Generated by Gradle"/> + </artifact> + <artifact name="netty-common-4.1.93.Final.pom"> + <sha256 value="42d883b13eb38cabf549616462c5f331f5333bf0c8fc9215744f83c0180a4f6b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.netty" name="netty-handler" version="4.1.93.Final"> + <artifact name="netty-handler-4.1.93.Final.jar"> + <sha256 value="4e5f563ae14ed713381816d582f5fcfd0615aefb29203486cdfb782d8a00a02b" origin="Generated by Gradle"/> + </artifact> + <artifact name="netty-handler-4.1.93.Final.pom"> + <sha256 value="84a1525cac0b4759efaef2997a47fe19b9b5642f9273fa0fd58a2e76c7a059fe" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.netty" name="netty-handler-proxy" version="4.1.93.Final"> + <artifact name="netty-handler-proxy-4.1.93.Final.jar"> + <sha256 value="2ac5f7fbefa0b73ef783889069344d5515505a14b2303be693c5002c486df2b4" origin="Generated by Gradle"/> + </artifact> + <artifact name="netty-handler-proxy-4.1.93.Final.pom"> + <sha256 value="6dc50da0e67f597812874f81eaa45404f7d076b8199ea9088932a85c1b610245" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.netty" name="netty-parent" version="4.1.93.Final"> + <artifact name="netty-parent-4.1.93.Final.pom"> + <sha256 value="b109cb76f375fedb8a9ef75ac58063170deb7ea2ddd024f466fef6dc65cdfcee" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.netty" name="netty-resolver" version="4.1.93.Final"> + <artifact name="netty-resolver-4.1.93.Final.jar"> + <sha256 value="e59770b66e81822e5d111ac4e544d7eb0c543e0a285f52628e53941acd8ed759" origin="Generated by Gradle"/> + </artifact> + <artifact name="netty-resolver-4.1.93.Final.pom"> + <sha256 value="5b350c3c91e9e55d29cbe68cfe4ef2116cc1f0328677ebf9f615bab7082c79f4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.netty" name="netty-transport" version="4.1.93.Final"> + <artifact name="netty-transport-4.1.93.Final.jar"> + <sha256 value="a5a78019bc1cd43dbc3c7b7cdd3801912ca26d1f498fb560514fee497864ba96" origin="Generated by Gradle"/> + </artifact> + <artifact name="netty-transport-4.1.93.Final.pom"> + <sha256 value="0dd62a0eb3cb1ea001a4d0426e4f5c08df1c70d9265675bffa5c583613422d43" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.netty" name="netty-transport-native-unix-common" version="4.1.93.Final"> + <artifact name="netty-transport-native-unix-common-4.1.93.Final.jar"> + <sha256 value="774165a1c4dbaacb17f9c1ad666b3569a6a59715ae828e7c3d47703f479a53e7" origin="Generated by Gradle"/> + </artifact> + <artifact name="netty-transport-native-unix-common-4.1.93.Final.pom"> + <sha256 value="15bc25b67ff0a49272b270ef2b8cffd620057ca16cb29dff7ad5661adca2ae12" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="io.perfmark" name="perfmark-api" version="0.26.0"> + <artifact name="perfmark-api-0.26.0.jar"> + <sha256 value="b7d23e93a34537ce332708269a0d1404788a5b5e1949e82f5535fce51b3ea95b" origin="Generated by Gradle"/> + </artifact> + <artifact name="perfmark-api-0.26.0.module"> + <sha256 value="31d832332474ce48150f5bae003343319136f336afd1076a289029319e3ea97a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="jakarta.activation" name="jakarta.activation-api" version="1.2.1"> + <artifact name="jakarta.activation-api-1.2.1.jar"> + <sha256 value="8b0a0f52fa8b05c5431921a063ed866efaa41dadf2e3a7ee3e1961f2b0d9645b" origin="Generated by Gradle"/> + </artifact> + <artifact name="jakarta.activation-api-1.2.1.pom"> + <sha256 value="42585cb07dda7f23aa04eb5e0940061944a246a67ad3d16942fbe569ff03cd31" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="jakarta.xml.bind" name="jakarta.xml.bind-api" version="2.3.2"> + <artifact name="jakarta.xml.bind-api-2.3.2.jar"> + <sha256 value="69156304079bdeed9fc0ae3b39389f19b3cc4ba4443bc80508995394ead742ea" origin="Generated by Gradle"/> + </artifact> + <artifact name="jakarta.xml.bind-api-2.3.2.pom"> + <sha256 value="b537b388dbab4cc0690b9d2fb0c74124d672531734567acf6e53130eab131ad6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="jakarta.xml.bind" name="jakarta.xml.bind-api-parent" version="2.3.2"> + <artifact name="jakarta.xml.bind-api-parent-2.3.2.pom"> + <sha256 value="15a55b7d537c9f9970aead28d2af97c059f65ff6102f76bbd29f1247dd8a6dfb" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="javax.annotation" name="javax.annotation-api" version="1.3.2"> + <artifact name="javax.annotation-api-1.3.2.jar"> + <sha256 value="e04ba5195bcd555dc95650f7cc614d151e4bcd52d29a10b8aa2197f3ab89ab9b" origin="Generated by Gradle"/> + </artifact> + <artifact name="javax.annotation-api-1.3.2.pom"> + <sha256 value="46a4a251ca406e78e4853d7a2bae83282844a4992851439ee9a1f23716f06b97" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="javax.inject" name="javax.inject" version="1"> + <artifact name="javax.inject-1-sources.jar"> + <sha256 value="c4b87ee2911c139c3daf498a781967f1eb2e75bc1a8529a2e7b328a15d0e433e" origin="Generated by Gradle"/> + </artifact> + <artifact name="javax.inject-1.jar"> + <sha256 value="91c77044a50c481636c32d916fd89c9118a72195390452c81065080f957de7ff" origin="Generated by Gradle"/> + </artifact> + <artifact name="javax.inject-1.pom"> + <sha256 value="943e12b100627804638fa285805a0ab788a680266531e650921ebfe4621a8bfa" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="junit" name="junit" version="4.13.2"> + <artifact name="junit-4.13.2-sources.jar"> + <sha256 value="34181df6482d40ea4c046b063cb53c7ffae94bdf1b1d62695bdf3adf9dea7e3a" origin="Generated by Gradle"/> + </artifact> + <artifact name="junit-4.13.2.jar"> + <sha256 value="8e495b634469d64fb8acfa3495a065cbacc8a0fff55ce1e31007be4c16dc57d3" origin="Generated by Gradle"/> + </artifact> + <artifact name="junit-4.13.2.pom"> + <sha256 value="569b6977ee4603c965c1c46c3058fa6e969291b0160eb6964dd092cd89eadd94" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="net.bytebuddy" name="byte-buddy" version="1.14.9"> + <artifact name="byte-buddy-1.14.9-sources.jar"> + <sha256 value="4039b20d06895bf0449161dd5f1cbbc2e81004af196d5b42c5536b91db8e0315" origin="Generated by Gradle"/> + </artifact> + <artifact name="byte-buddy-1.14.9.jar"> + <sha256 value="377352e253282bf86f731ac90ed88348e8f40a63ce033c00a85982de7e790e6f" origin="Generated by Gradle"/> + </artifact> + <artifact name="byte-buddy-1.14.9.pom"> + <sha256 value="2d6772910d16169bd4c9229e8a365cf54e192bc620cbcfa7f84271f5d115e815" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="net.bytebuddy" name="byte-buddy-agent" version="1.14.9"> + <artifact name="byte-buddy-agent-1.14.9-sources.jar"> + <sha256 value="01ba24ea9e0c83e6e8a6d96d5476bd2860a26bc98994af747a6cbd7b015ca85f" origin="Generated by Gradle"/> + </artifact> + <artifact name="byte-buddy-agent-1.14.9.jar"> + <sha256 value="11ed107d4b78e55f8c3d34250494375081a29bc125a1f5c56db582ccdd48835f" origin="Generated by Gradle"/> + </artifact> + <artifact name="byte-buddy-agent-1.14.9.pom"> + <sha256 value="ab11e9a20231d1866a7d4035eb0b0075b80360d8cddc5d0a373c39584e9cd247" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="net.bytebuddy" name="byte-buddy-parent" version="1.14.9"> + <artifact name="byte-buddy-parent-1.14.9.pom"> + <sha256 value="66b8342251d35f2063e69316f63bafcb056342fe75e921c0f465c2d96593535e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="net.java" name="jvnet-parent" version="1"> + <artifact name="jvnet-parent-1.pom"> + <sha256 value="281440811268e65d9e266b3cc898297e214e04f09740d0386ceeb4a8923d63bf" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="net.java" name="jvnet-parent" version="3"> + <artifact name="jvnet-parent-3.pom"> + <sha256 value="30f5789efa39ddbf96095aada3fc1260c4561faf2f714686717cb2dc5049475a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="net.java.dev.jna" name="jna" version="5.6.0"> + <artifact name="jna-5.6.0.jar"> + <sha256 value="5557e235a8aa2f9766d5dc609d67948f2a8832c2d796cea9ef1d6cbe0b3b7eaf" origin="Generated by Gradle"/> + </artifact> + <artifact name="jna-5.6.0.pom"> + <sha256 value="5fe81b0255978f24616d37b10608b79498a5f3073e1d9b2038d8736a831f2608" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="net.java.dev.jna" name="jna-platform" version="5.6.0"> + <artifact name="jna-platform-5.6.0.jar"> + <sha256 value="9ecea8bf2b1b39963939d18b70464eef60c508fed8820f9dcaba0c35518eabf7" origin="Generated by Gradle"/> + </artifact> + <artifact name="jna-platform-5.6.0.pom"> + <sha256 value="1beb35cb4184e6c906a7e32eaebd852dd3da0a263962e99134ab945832394e28" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="net.ltgt.gradle.incap" name="incap" version="0.2"> + <artifact name="incap-0.2.jar"> + <sha256 value="b625b9806b0f1e4bc7a2e3457119488de3cd57ea20feedd513db070a573a4ffd" origin="Generated by Gradle"/> + </artifact> + <artifact name="incap-0.2.pom"> + <sha256 value="1a4a08a1e88d32052cd82dc2f740b34d3048e2c0e6a7c2bfe2309ed00771f73a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="net.sf.jopt-simple" name="jopt-simple" version="4.9"> + <artifact name="jopt-simple-4.9.jar"> + <sha256 value="26c5856e954b5f864db76f13b86919b59c6eecf9fd930b96baa8884626baf2f5" origin="Generated by Gradle"/> + </artifact> + <artifact name="jopt-simple-4.9.pom"> + <sha256 value="7af7e2d8b24b4798f04c2b7da24c9fbd1b7557b4e017c2054481565916079092" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="net.sf.kxml" name="kxml2" version="2.3.0"> + <artifact name="kxml2-2.3.0.jar"> + <sha256 value="f264dd9f79a1fde10ce5ecc53221eff24be4c9331c830b7d52f2f08a7b633de2" origin="Generated by Gradle"/> + </artifact> + <artifact name="kxml2-2.3.0.pom"> + <sha256 value="31ce606f4e9518936299bb0d27c978fa61e185fd1de7c9874fe959a53e34a685" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache" name="apache" version="13"> + <artifact name="apache-13.pom"> + <sha256 value="ff513db0361fd41237bef4784968bc15aae478d4ec0a9496f811072ccaf3841d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache" name="apache" version="15"> + <artifact name="apache-15.pom"> + <sha256 value="36c2f2f979ac67b450c0cb480e4e9baf6b40f3a681f22ba9692287d1139ad494" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache" name="apache" version="18"> + <artifact name="apache-18.pom"> + <sha256 value="7831307285fd475bbc36b20ae38e7882f11c3153b1d5930f852d44eda8f33c17" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache" name="apache" version="21"> + <artifact name="apache-21.pom"> + <sha256 value="af10c108da014f17cafac7b52b2b4b5a3a1c18265fa2af97a325d9143537b380" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache" name="apache" version="23"> + <artifact name="apache-23.pom"> + <sha256 value="bc10624e0623f36577fac5639ca2936d3240ed152fb6d8d533ab4d270543491c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache" name="apache" version="29"> + <artifact name="apache-29.pom"> + <sha256 value="3e49037174820bbd0df63420a977255886398954c2a06291fa61f727ac35b377" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache.commons" name="commons-compress" version="1.21"> + <artifact name="commons-compress-1.21.jar"> + <sha256 value="6aecfd5459728a595601cfa07258d131972ffc39b492eb48bdd596577a2f244a" origin="Generated by Gradle"/> + </artifact> + <artifact name="commons-compress-1.21.pom"> + <sha256 value="675bb023c9beedde3232949979b9742a5fea946280a55a1b462d4ca7801088cd" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache.commons" name="commons-parent" version="34"> + <artifact name="commons-parent-34.pom"> + <sha256 value="3a2e69d06d641d1f3b293126dc9e2e4ea6563bf8c36c87e0ab6fa4292d04b79c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache.commons" name="commons-parent" version="35"> + <artifact name="commons-parent-35.pom"> + <sha256 value="7098a1ab8336ecd4c9dc21cbbcac869f82c66f64b8ac4f7988d41b4fcb44e49a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache.commons" name="commons-parent" version="42"> + <artifact name="commons-parent-42.pom"> + <sha256 value="cd313494c670b483ec256972af1698b330e598f807002354eb765479f604b09c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache.commons" name="commons-parent" version="52"> + <artifact name="commons-parent-52.pom"> + <sha256 value="75dbe8f34e98e4c3ff42daae4a2f9eb4cbcd3b5f1047d54460ace906dbb4502e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache.commons" name="commons-parent" version="58"> + <artifact name="commons-parent-58.pom"> + <sha256 value="2d4b12e18899063abd7c75278b5fa97a3729d80878ceecb6a40d946e9c0d5590" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache.httpcomponents" name="httpclient" version="4.5.14"> + <artifact name="httpclient-4.5.14.jar"> + <sha256 value="c8bc7e1c51a6d4ce72f40d2ebbabf1c4b68bfe76e732104b04381b493478e9d6" origin="Generated by Gradle"/> + </artifact> + <artifact name="httpclient-4.5.14.pom"> + <sha256 value="f18355af4cf80a8a4ef04ebd742a47e90a7eaf080c725b2095dbc4fc5dbdefb7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache.httpcomponents" name="httpclient" version="4.5.6"> + <artifact name="httpclient-4.5.6.jar"> + <sha256 value="c03f813195e7a80e3608d0ddd8da80b21696a4c92a6a2298865bf149071551c7" origin="Generated by Gradle"/> + </artifact> + <artifact name="httpclient-4.5.6.pom"> + <sha256 value="7efc1241e73e7fbb268bfd33242d11ebd3ca07061d7d85f2962dc32a0f0b8855" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache.httpcomponents" name="httpcomponents-client" version="4.5.14"> + <artifact name="httpcomponents-client-4.5.14.pom"> + <sha256 value="5bad1de4f101447659f89d089868ccbad64a68cc503d2d65410b51f6904aa061" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache.httpcomponents" name="httpcomponents-client" version="4.5.6"> + <artifact name="httpcomponents-client-4.5.6.pom"> + <sha256 value="b042b41f2391edb00d35f7f4e509aed2123648c1d246ce58d0f7b905c9fe1f73" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache.httpcomponents" name="httpcomponents-core" version="4.4.16"> + <artifact name="httpcomponents-core-4.4.16.pom"> + <sha256 value="f2d75a2c2d423ad18539bf21656d56f88a4091944a662fcaf159d5ae283db7f7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache.httpcomponents" name="httpcomponents-parent" version="10"> + <artifact name="httpcomponents-parent-10.pom"> + <sha256 value="caaf967d94afb21753f36082c6086206bd1f48825ff596932cceba72b65d39fa" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache.httpcomponents" name="httpcomponents-parent" version="11"> + <artifact name="httpcomponents-parent-11.pom"> + <sha256 value="a901f87b115c55070c7ee43efff63e20e7b02d30af2443ae292bf1f4e532d3aa" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache.httpcomponents" name="httpcore" version="4.4.16"> + <artifact name="httpcore-4.4.16.jar"> + <sha256 value="6c9b3dd142a09dc468e23ad39aad6f75a0f2b85125104469f026e52a474e464f" origin="Generated by Gradle"/> + </artifact> + <artifact name="httpcore-4.4.16.pom"> + <sha256 value="3cbad849b35dacfe6cec31adada2c623c026c3261141b0d26eec7e399c6cd7fa" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.apache.httpcomponents" name="httpmime" version="4.5.6"> + <artifact name="httpmime-4.5.6.jar"> + <sha256 value="0b2b1102c18d3c7e05a77214b9b7501a6f6056174ae5604e0e256776eda7553e" origin="Generated by Gradle"/> + </artifact> + <artifact name="httpmime-4.5.6.pom"> + <sha256 value="dfbfd6ffe2a784ca9817c46365aa7f8a578320b805bde39d6f55a0b09d8aa8ca" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.bitbucket.b_c" name="jose4j" version="0.7.0"> + <artifact name="jose4j-0.7.0.jar"> + <sha256 value="eb14f69c0395d4a106c6c46fe6dff080c4608ccabc99b1f03933d374383d9bbe" origin="Generated by Gradle"/> + </artifact> + <artifact name="jose4j-0.7.0.pom"> + <sha256 value="13128ccf90fb31f18d8109b484516649d5db0e47284d860e71a4cf3148153ed8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.bitbucket.b_c" name="jose4j" version="0.9.5"> + <artifact name="jose4j-0.9.5.jar"> + <sha256 value="808fb3166f3e67dad9811c331029ab1681242fd52b735bc3f33f281167fcc72e" origin="Generated by Gradle"/> + </artifact> + <artifact name="jose4j-0.9.5.pom"> + <sha256 value="bad024180a1b469cbd94e5f2db12841bcac5443d9545607f673cfde677c1d872" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.bouncycastle" name="bcpkix-jdk18on" version="1.77"> + <artifact name="bcpkix-jdk18on-1.77.jar"> + <sha256 value="1ac7fe8efd5b2f38cdc165be5a0675734fe44808dab92707201f03a535d6f1b8" origin="Generated by Gradle"/> + </artifact> + <artifact name="bcpkix-jdk18on-1.77.pom"> + <sha256 value="8fb0926f02e2c4b2dc52e47ebb093f92f1d3bb6f149c2a5cca5e2a648d2c498d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.bouncycastle" name="bcprov-jdk15on" version="1.70"> + <artifact name="bcprov-jdk15on-1.70-sources.jar"> + <sha256 value="0252e39814e4403b5d91a7386c3a5ac3e1fe65d43c2d25fed8d45e8eebab2696" origin="Generated by Gradle"/> + </artifact> + <artifact name="bcprov-jdk15on-1.70.jar"> + <sha256 value="8f3c20e3e2d565d26f33e8d4857a37d0d7f8ac39b62a7026496fcab1bdac30d4" origin="Generated by Gradle"/> + </artifact> + <artifact name="bcprov-jdk15on-1.70.pom"> + <sha256 value="6df4b5b76d9062017664ad0ca285e57154ae803607cb89c970b39cc0e016abb0" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.bouncycastle" name="bcprov-jdk18on" version="1.77"> + <artifact name="bcprov-jdk18on-1.77.jar"> + <sha256 value="dabb98c24d72c9b9f585633d1df9c5cd58d9ad373d0cd681367e6a603a495d58" origin="Generated by Gradle"/> + </artifact> + <artifact name="bcprov-jdk18on-1.77.pom"> + <sha256 value="ad1382cfcd03bcdd8be13913c02f44fd469d0a76a53cf2caef5be180adc36b23" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.bouncycastle" name="bcutil-jdk18on" version="1.77"> + <artifact name="bcutil-jdk18on-1.77.jar"> + <sha256 value="947673bcbc5a8dde2d2fa688a5b7598d0ca6e2a74a7ea30cd93f04f6b3ad68f8" origin="Generated by Gradle"/> + </artifact> + <artifact name="bcutil-jdk18on-1.77.pom"> + <sha256 value="163dfa6632ffb928a705ca83722350cacea49ccd623ce73645a5cc3b0fa9e6e8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.checkerframework" name="checker-compat-qual" version="2.5.5"> + <artifact name="checker-compat-qual-2.5.5.jar"> + <sha256 value="11d134b245e9cacc474514d2d66b5b8618f8039a1465cdc55bbc0b34e0008b7a" origin="Generated by Gradle"/> + </artifact> + <artifact name="checker-compat-qual-2.5.5.pom"> + <sha256 value="42f21ebd9183be049ee5afc822b345403a5da764037875734a039b0d6e0353be" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.checkerframework" name="checker-qual" version="2.5.8"> + <artifact name="checker-qual-2.5.8.pom"> + <sha256 value="33ac6a0f1341ae96647c7d4465f4aa3d24fe97d2697bcee2ceae6fc8b5ef2c3c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.checkerframework" name="checker-qual" version="3.33.0"> + <artifact name="checker-qual-3.33.0.jar"> + <sha256 value="e316255bbfcd9fe50d165314b85abb2b33cb2a66a93c491db648e498a82c2de1" origin="Generated by Gradle"/> + </artifact> + <artifact name="checker-qual-3.33.0.module"> + <sha256 value="e8521d75625d41272c767d262a153ac163cc505b66644a2ef705fa8949ffb4e5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.checkerframework" name="checker-qual" version="3.41.0"> + <artifact name="checker-qual-3.41.0.jar"> + <sha256 value="2f9f245bf68e4259d610894f2406dc1f6363dc639302bd566e8272e4f4541172" origin="Generated by Gradle"/> + </artifact> + <artifact name="checker-qual-3.41.0.module"> + <sha256 value="b38672c17f455276b210ed3402ff92dce666770b1a8c610c7cc34ad54c532d20" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.checkerframework" name="checker-qual" version="3.42.0"> + <artifact name="checker-qual-3.42.0-sources.jar"> + <sha256 value="efb65eb479f61f53c6dcafbd42ed59dad09b0a0d5a7f44b7bc68df25c2dcf8fd" origin="Generated by Gradle"/> + </artifact> + <artifact name="checker-qual-3.42.0.jar"> + <sha256 value="ccaedd33af0b7894d9f2f3b644f4d19e43928e32902e61ac4d10777830f5aac7" origin="Generated by Gradle"/> + </artifact> + <artifact name="checker-qual-3.42.0.module"> + <sha256 value="e0fa622b7de63eae11047ef6e91b4c2ad0f1f0e13cb903ff52080a47f57a5746" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.codehaus.groovy" name="groovy" version="3.0.17"> + <artifact name="groovy-3.0.17.jar"> + <sha256 value="9ed2a565a8592a9f63f410afed892d0ca6bec1ff96277ce4cc7f3e2e366e794f" origin="Generated by Gradle"/> + </artifact> + <artifact name="groovy-3.0.17.pom"> + <sha256 value="207afde880172806c0a3b20f122d9679f175f22d15b05e87f589f7255dd38362" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.codehaus.mojo" name="animal-sniffer-annotations" version="1.23"> + <artifact name="animal-sniffer-annotations-1.23.jar"> + <sha256 value="9ffe526bf43a6348e9d8b33b9cd6f580a7f5eed0cf055913007eda263de974d0" origin="Generated by Gradle"/> + </artifact> + <artifact name="animal-sniffer-annotations-1.23.pom"> + <sha256 value="5610db06b733641acbc7a0c48a80c40069db627bad043f8c7c8d7afb4f6a3d27" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.codehaus.mojo" name="animal-sniffer-parent" version="1.23"> + <artifact name="animal-sniffer-parent-1.23.pom"> + <sha256 value="6b7f054ab86a87f8e2599f35808b0989922e86b6cab13988021cd12640a4b404" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.codehaus.mojo" name="mojo-parent" version="74"> + <artifact name="mojo-parent-74.pom"> + <sha256 value="1472325a16f0b1bdabed21fa4839372964944610294ce2681b2059edc654f2b3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.conscrypt" name="conscrypt-android" version="2.5.2"> + <artifact name="conscrypt-android-2.5.2-sources.jar"> + <sha256 value="d7b34504e1c77dbe866f08b1f12e0908c1c3456e8d7eabef1a819b78ac1ad406" origin="Generated by Gradle"/> + </artifact> + <artifact name="conscrypt-android-2.5.2.aar"> + <sha256 value="42d18979caf53f5ef68548c76d4c98b41adb910a32ad9448133f9c5b20bd65a3" origin="Generated by Gradle"/> + </artifact> + <artifact name="conscrypt-android-2.5.2.pom"> + <sha256 value="bfdf7f13fb78550ffff265adc1d7d9b8e37dbdd60b3fd7bfd410ea2f768ed124" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.conscrypt" name="conscrypt-openjdk-uber" version="2.5.2"> + <artifact name="conscrypt-openjdk-uber-2.5.2.jar"> + <sha256 value="eaf537d98e033d0f0451cd1b8cc74e02d7b55ec882da63c88060d806ba89c348" origin="Generated by Gradle"/> + </artifact> + <artifact name="conscrypt-openjdk-uber-2.5.2.pom"> + <sha256 value="b5fd548732f932545d777890eb995222bfe866232351be908137e39c3c672d8b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.ec4j.core" name="ec4j-core" version="0.3.0"> + <artifact name="ec4j-core-0.3.0.jar"> + <sha256 value="cadef0207077074b11a12be442f89ab6cf93fbc2f848702d9371a9611414d558" origin="Generated by Gradle"/> + </artifact> + <artifact name="ec4j-core-0.3.0.pom"> + <sha256 value="fd1b5d4ca1511b3cbc9c91af75ef361228b4f9fb100192d3c865471844ddc6e9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.ec4j.core" name="ec4j-core-parent" version="0.3.0"> + <artifact name="ec4j-core-parent-0.3.0.pom"> + <sha256 value="90a311043353c087db3ff2f34802967358a2e488245d887b39c21b66f73ede65" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.eclipse.ee4j" name="project" version="1.0.2"> + <artifact name="project-1.0.2.pom"> + <sha256 value="7495a07a797e88e43c3bc1a87421bd8b1fc55e32291fa18e4e32d8031ddc873f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.eclipse.ee4j" name="project" version="1.0.5"> + <artifact name="project-1.0.5.pom"> + <sha256 value="916b4794d8d8220a59a3fdf6a64dbe794aeb23395e888b81ae36a9b5a2c591a6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.glassfish.jaxb" name="jaxb-bom" version="2.3.2"> + <artifact name="jaxb-bom-2.3.2.pom"> + <sha256 value="a1018bb54678ed9f5acb2f7a4084e385ff510201f4e9dbf5f75dc6a675f66be7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.glassfish.jaxb" name="jaxb-runtime" version="2.3.2"> + <artifact name="jaxb-runtime-2.3.2.jar"> + <sha256 value="e6e0a1e89fb6ff786279e6a0082d5cef52dc2ebe67053d041800737652b4fd1b" origin="Generated by Gradle"/> + </artifact> + <artifact name="jaxb-runtime-2.3.2.pom"> + <sha256 value="9448a5ad7fa68a6083dfbe4f42c8c83e082b9202a105401fc68e944c26548b34" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.glassfish.jaxb" name="txw2" version="2.3.2"> + <artifact name="txw2-2.3.2.jar"> + <sha256 value="4a6a9f483388d461b81aa9a28c685b8b74c0597993bf1884b04eddbca95f48fe" origin="Generated by Gradle"/> + </artifact> + <artifact name="txw2-2.3.2.pom"> + <sha256 value="a79dd002fb038183ff286a2635be2e68c103b87e0e64717d8d44bfd017fd33ea" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.gradle.toolchains" name="foojay-resolver" version="0.8.0"> + <artifact name="foojay-resolver-0.8.0.jar"> + <sha256 value="f90e69351638e90b9ec9848e759d218635907c092541191150037b0b22e5140c" origin="Generated by Gradle"/> + </artifact> + <artifact name="foojay-resolver-0.8.0.module"> + <sha256 value="8c3ccf54da072c64a40e06882aaa652336cb29eec2ea230f06d6843abb384d51" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.gradle.toolchains.foojay-resolver-convention" name="org.gradle.toolchains.foojay-resolver-convention.gradle.plugin" version="0.8.0"> + <artifact name="org.gradle.toolchains.foojay-resolver-convention.gradle.plugin-0.8.0.pom"> + <sha256 value="3b672237bd9cc1e8e8ca86ef5a7920a678f69d04d2f4bfbd0c5a2e79d45c5cb5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.hamcrest" name="hamcrest-core" version="1.3"> + <artifact name="hamcrest-core-1.3-sources.jar"> + <sha256 value="e223d2d8fbafd66057a8848cc94222d63c3cedd652cc48eddc0ab5c39c0f84df" origin="Generated by Gradle"/> + </artifact> + <artifact name="hamcrest-core-1.3.jar"> + <sha256 value="66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9" origin="Generated by Gradle"/> + </artifact> + <artifact name="hamcrest-core-1.3.pom"> + <sha256 value="fde386a7905173a1b103de6ab820727584b50d0e32282e2797787c20a64ffa93" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.hamcrest" name="hamcrest-integration" version="1.3"> + <artifact name="hamcrest-integration-1.3-sources.jar"> + <sha256 value="0827a37533a135bfab9c27e0f4b6d6fb1394856e842ef20a1693d3bfeb061365" origin="Generated by Gradle"/> + </artifact> + <artifact name="hamcrest-integration-1.3.jar"> + <sha256 value="70f418efbb506c5155da5f9a5a33262ea08a9e4d7fea186aa9015c41a7224ac2" origin="Generated by Gradle"/> + </artifact> + <artifact name="hamcrest-integration-1.3.pom"> + <sha256 value="42f0be9bf98c12dacdcb99dd141d83d4dc5bb7c37a6f26684cd3ff2287667fba" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.hamcrest" name="hamcrest-library" version="1.3"> + <artifact name="hamcrest-library-1.3-sources.jar"> + <sha256 value="1c0ff84455f539eb3c29a8c430de1f6f6f1ba4b9ab39ca19b195f33203cd539c" origin="Generated by Gradle"/> + </artifact> + <artifact name="hamcrest-library-1.3.jar"> + <sha256 value="711d64522f9ec410983bd310934296da134be4254a125080a0416ec178dfad1c" origin="Generated by Gradle"/> + </artifact> + <artifact name="hamcrest-library-1.3.pom"> + <sha256 value="1ceb4bfb0f098ae29b935044b2363e11323313fe3ed2055df8b79737d5056277" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.hamcrest" name="hamcrest-parent" version="1.3"> + <artifact name="hamcrest-parent-1.3.pom"> + <sha256 value="6d535f94efb663bdb682c9f27a50335394688009642ba7a9677504bc1be4129b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jdom" name="jdom2" version="2.0.6"> + <artifact name="jdom2-2.0.6.jar"> + <sha256 value="1345f11ba606d15603d6740551a8c21947c0215640770ec67271fe78bea97cf5" origin="Generated by Gradle"/> + </artifact> + <artifact name="jdom2-2.0.6.pom"> + <sha256 value="47b23a79fe336b741b82434c6e049d68165256e405e75c10921fd72fa8a65d8d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains" name="annotations" version="13.0"> + <artifact name="annotations-13.0.jar"> + <sha256 value="ace2a10dc8e2d5fd34925ecac03e4988b2c0f851650c94b8cef49ba1bd111478" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotations-13.0.pom"> + <sha256 value="965aeb2bedff369819bdde1bf7a0b3b89b8247dd69c88b86375d76163bb8c397" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains" name="annotations" version="23.0.0"> + <artifact name="annotations-23.0.0.jar"> + <sha256 value="7b0f19724082cbfcbc66e5abea2b9bc92cf08a1ea11e191933ed43801eb3cd05" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotations-23.0.0.pom"> + <sha256 value="c9490f655132328df2cfbcfdf743f53fc3916d6c1d10437175a6ca6e3a67771c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.compose.runtime" name="runtime" version="1.6.10"> + <artifact name="runtime-1.6.10.module"> + <sha256 value="e0c536361b4cbe06f5d740e15db166d34c005709e9a51c867c2cfdb7c98afbec" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.intellij.deps" name="trove4j" version="1.0.20200330"> + <artifact name="trove4j-1.0.20200330.jar"> + <sha256 value="c5fd725bffab51846bf3c77db1383c60aaaebfe1b7fe2f00d23fe1b7df0a439d" origin="Generated by Gradle"/> + </artifact> + <artifact name="trove4j-1.0.20200330.pom"> + <sha256 value="87721cbaa65a3c97d8b1ba9d207840f164c9fe38759fc9ea10ffe26565f8d3e9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-android-extensions-runtime" version="2.0.0"> + <artifact name="kotlin-android-extensions-runtime-2.0.0-sources.jar"> + <sha256 value="418ea1adaaaf4537d180cc25e0b1c0d02a54fa3a38f88bd7625eb2307aa447dd" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-android-extensions-runtime-2.0.0.jar"> + <sha256 value="787c67e147f66f9c274267af700750dcc3654721e40d174232b7e6f4c47bbe9c" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-android-extensions-runtime-2.0.0.pom"> + <sha256 value="aaedf6b9f9057ed474e0c2768936798f29275251be88cf06e8e081f6603093b4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-bom" version="1.8.21"> + <artifact name="kotlin-bom-1.8.21.pom"> + <sha256 value="43f52cd839d8fd872f240491d38fa7df170a87a36e105e700dd939d15572a8ef" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-bom" version="1.8.22"> + <artifact name="kotlin-bom-1.8.22.pom"> + <sha256 value="c8d794eb761888d34369e677de8eaba0b01f9e8a756cfbff53215ccfa5c58c3f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-bom" version="1.9.20"> + <artifact name="kotlin-bom-1.9.20.pom"> + <sha256 value="ec72aeeff33f86dbdfa59505c8349ba82d928ce282af5a1de3d890ebe007345d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-build-common" version="2.0.0"> + <artifact name="kotlin-build-common-2.0.0.jar"> + <sha256 value="26868d37837621259993cd8b4587bfffae86b17e4f211d5baa0bf605248a2412" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-build-common-2.0.0.pom"> + <sha256 value="370f9ceb7c9621495447874109c586f58308b945b9d921d87af5bbb48df54d35" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-build-statistics" version="2.0.0"> + <artifact name="kotlin-build-statistics-2.0.0.jar"> + <sha256 value="f089b5f057f7c78630107bf4661f75b68cf1ce7492f415bdf2b50bc2b47ef815" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-build-statistics-2.0.0.pom"> + <sha256 value="9ac80f8df65172a45f80f5b421001377098133b7cb5d73387cffbf2b2239be69" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-build-tools-api" version="2.0.0"> + <artifact name="kotlin-build-tools-api-2.0.0.jar"> + <sha256 value="8be57288054decd4bca5a2dcb89e4e8e896c2520e45a2488765d813ffd229f11" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-build-tools-api-2.0.0.pom"> + <sha256 value="cc39e9e0c9bbeec5a0ccfe0a78734c541c00fca3027c6de8ebc13bade0f5400b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-build-tools-impl" version="2.0.0"> + <artifact name="kotlin-build-tools-impl-2.0.0.jar"> + <sha256 value="551cce7299ed378a4fed97e6c26179af10f18b7b263f9599ae0086faa160446b" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-build-tools-impl-2.0.0.pom"> + <sha256 value="21778645ed218ca2937782991610716826efdbd490dc8129e3534705fb6a1002" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-compiler-embeddable" version="1.9.10"> + <artifact name="kotlin-compiler-embeddable-1.9.10.jar"> + <sha256 value="b6d3965fdb3fc2a5f8d965681c215c37552b28ae5ad19fcadbc1568c9b65dab4" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-compiler-embeddable-1.9.10.pom"> + <sha256 value="c5456b1c82a3ef2153b9939f71d190215f6e53a8192d26e8d76eec23325556bd" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-compiler-embeddable" version="2.0.0"> + <artifact name="kotlin-compiler-embeddable-2.0.0.jar"> + <sha256 value="eb8ae09df38e212eec3965cafa97ab08112773fe2e870ebeb6131b8f69bfb92e" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-compiler-embeddable-2.0.0.pom"> + <sha256 value="13c04c2c983a8faab531b64d0662db247fd35a1d88489330a1c29dc4a91e569e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-compiler-runner" version="2.0.0"> + <artifact name="kotlin-compiler-runner-2.0.0.jar"> + <sha256 value="18f22b00697b84ef45a41d2b0a572261f299eaa7dcdda819d8773948e4472474" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-compiler-runner-2.0.0.pom"> + <sha256 value="42e6632358fa62be99bb8e4a278707e6b810dfc71ad3e5eea17e54ae39c83aac" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-daemon-client" version="2.0.0"> + <artifact name="kotlin-daemon-client-2.0.0.jar"> + <sha256 value="dfa5ae84fc665b0478475fd9a63f2f984a9155ce1bfb4dc2abea278c6a0f2d94" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-daemon-client-2.0.0.pom"> + <sha256 value="dbbc417fbbe94efab6a05a081d0f6336dacffa9d0426375e2a1781c57c5c2991" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-daemon-embeddable" version="1.9.10"> + <artifact name="kotlin-daemon-embeddable-1.9.10.jar"> + <sha256 value="79bd4bf388da4430b0a9be86d2f72a111110941965edd478e99f3ae083156116" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-daemon-embeddable-1.9.10.pom"> + <sha256 value="c733493de5f7bf1e082cec0edf474b34d90b074740c3b3d0d383cf81b7522f29" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-daemon-embeddable" version="2.0.0"> + <artifact name="kotlin-daemon-embeddable-2.0.0.jar"> + <sha256 value="b907a36e9cfa587ca0523793d89eb2ea9bb545456935f180e12422486c14049f" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-daemon-embeddable-2.0.0.pom"> + <sha256 value="ce23393a051fa5e94275854e12cee7ac1c1e9f641bd71ace81cba6fd54f9bc1a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin" version="2.0.0"> + <artifact name="kotlin-gradle-plugin-2.0.0-gradle85.jar"> + <sha256 value="fa9f364fb3ff1330ea2908e6ddfe66ee3d20abe975d61ae9f81e1e6bd276eaf7" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-gradle-plugin-2.0.0.module"> + <sha256 value="e45806404e7e0c398a79bb5800205cb6ab2e4ad8d9b604170d33437c45351189" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-annotations" version="2.0.0"> + <artifact name="kotlin-gradle-plugin-annotations-2.0.0.jar"> + <sha256 value="42b614cf70363be1195490e2567ad38cf26ccb32cf45db90509434e9621cb633" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-gradle-plugin-annotations-2.0.0.pom"> + <sha256 value="214751a3993b10e16c72ef63f961f56d37a1c751aaef234e269172e1c40e528f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-api" version="2.0.0"> + <artifact name="kotlin-gradle-plugin-api-2.0.0.jar"> + <sha256 value="f95693a82de0bbfb06d6b1eac364954b68bf488ba0c8cc2ff273976bb28ee223" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-gradle-plugin-api-2.0.0.module"> + <sha256 value="9f6ac6f6dcb3c3d933fde92e4de93381b78d3212ae68eb66702da812bf6ecfa9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-idea" version="2.0.0"> + <artifact name="kotlin-gradle-plugin-idea-2.0.0.jar"> + <sha256 value="c1f4ea0c19267f1eed474b541b07715c49167acfbf027a8a2fd07cbbc81b8e72" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-gradle-plugin-idea-2.0.0.module"> + <sha256 value="4163bd47a34ba1bd74c323577fe8426b78d5662b760658106352b791bad4dcf8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-idea-proto" version="2.0.0"> + <artifact name="kotlin-gradle-plugin-idea-proto-2.0.0.jar"> + <sha256 value="51d31fe50bc89ac080d49bc87cc77f139586cfcb1098d4aaafbbc72a71d8e207" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-gradle-plugin-idea-proto-2.0.0.pom"> + <sha256 value="acbec7892f527cb610b62493551221e4d0d08e0b38973464bd6d747f1bab227a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugin-model" version="2.0.0"> + <artifact name="kotlin-gradle-plugin-model-2.0.0.jar"> + <sha256 value="b7b436caef0b0d5f571ddd88214418cf8580a34ec8e81f22503cef35755552d9" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-gradle-plugin-model-2.0.0.module"> + <sha256 value="ecc56ff4219215ae9626e06d51aa9a3ef59bf5e6d4509260328593c059805b06" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-gradle-plugins-bom" version="2.0.0"> + <artifact name="kotlin-gradle-plugins-bom-2.0.0.module"> + <sha256 value="eab8df114dc10987acd3093ca2ae310b753a13b718a263d5b5d83a6595c04bda" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-gradle-plugins-bom-2.0.0.pom"> + <sha256 value="c4a0586af77f0fb4f7e367033a02ac718beff0d0193cc7989583a982aa2752f2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-klib-commonizer-api" version="2.0.0"> + <artifact name="kotlin-klib-commonizer-api-2.0.0.jar"> + <sha256 value="1e1b57dc1f802aa778f5f46054a253213d8b03c58357e347f08222a2d1950175" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-klib-commonizer-api-2.0.0.pom"> + <sha256 value="72aea51ffd69e7a6d7de7e2ca9ac192c69918f5a0339974caf5e9fa4c762cd58" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-klib-commonizer-embeddable" version="2.0.0"> + <artifact name="kotlin-klib-commonizer-embeddable-2.0.0.jar"> + <sha256 value="687bca71931939389e9d0200f95ecc1508e0d41851ede51f4610f876d037a1dd" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-klib-commonizer-embeddable-2.0.0.pom"> + <sha256 value="7aab0a39bad95150823df619659fc49ba951de0b7dfa683fce27bcea18da3c9e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-native-prebuilt" version="2.0.0"> + <artifact name="kotlin-native-prebuilt-2.0.0-linux-x86_64.tar.gz"> + <sha256 value="1c1e18487ffcee3c19a1ca6500dd6589efdae844bade5ec071fbeca801292aa3" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-native-prebuilt-2.0.0-macos-aarch64.tar.gz"> + <sha256 value="d60bea435a0b23fd1bb1b4ad6ae50c778b52cf300cb3421471b252e44bf184c5" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-native-prebuilt-2.0.0.pom"> + <sha256 value="c83bc3c52dd174531b5dd48e167e00fcaa9a1459752e8ce21d9e381b893af406" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-native-utils" version="2.0.0"> + <artifact name="kotlin-native-utils-2.0.0.jar"> + <sha256 value="20d5904b4f0fa20624cab109ae303dff039142651b77901e0e14aae5feeb0ae6" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-native-utils-2.0.0.pom"> + <sha256 value="56317d6fc49729f42e935d1f8c9769d3ad855264563a57a691503ba80ae3d452" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-parcelize-compiler" version="2.0.0"> + <artifact name="kotlin-parcelize-compiler-2.0.0.jar"> + <sha256 value="a5014fb9369dca119d88df58205f7aa5ddc2589aa2ad5daa90c2f95e3ff4e86e" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-parcelize-compiler-2.0.0.pom"> + <sha256 value="50d45000569ca3ba08735a1c59bdc305dab3c0e2333ff18def9b111a040b3953" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-parcelize-runtime" version="2.0.0"> + <artifact name="kotlin-parcelize-runtime-2.0.0-sources.jar"> + <sha256 value="b7cf21217b9bbfbea21dd944c163454a8592344bef5b0b92ba2fa46aa1e97660" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-parcelize-runtime-2.0.0.jar"> + <sha256 value="d75402374e546f61db07a8467cac7537b59d22e0860ffeed370ce2a1aeefba21" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-parcelize-runtime-2.0.0.pom"> + <sha256 value="54d7c62af931be9993df74a08facd1de3f161b60f0c20a58281fd1133ee26bea" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.6.10"> + <artifact name="kotlin-reflect-1.6.10.jar"> + <sha256 value="3277ac102ae17aad10a55abec75ff5696c8d109790396434b496e75087854203" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-reflect-1.6.10.pom"> + <sha256 value="57905524274a00ae028aaccc27283f6bc5925a934a046c1cc5d06c8ee4d6d5a9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.8.21"> + <artifact name="kotlin-reflect-1.8.21.jar"> + <sha256 value="8a6cd5a3cf092acee274ce2c444dc36eefdb631579859dd4d857b3309a529c91" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-reflect-1.8.21.pom"> + <sha256 value="39d78db33222cdbb32b618c7c5223d4457cc19742856d116a85090e4a96f623a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-reflect" version="1.9.20"> + <artifact name="kotlin-reflect-1.9.20.jar"> + <sha256 value="49b66f9a89d50fd2954c2e8aeac80e4f488b0a09322a25efad6261576713dc0f" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-reflect-1.9.20.pom"> + <sha256 value="942b5e8602d317ec13652f1c0222052bb90817f28cf6fe9d47112f09b3e8e67d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-script-runtime" version="1.9.10"> + <artifact name="kotlin-script-runtime-1.9.10.jar"> + <sha256 value="2a6087375be9bdfaaadb4ba4be9833bba0de8edab1255c916642acaabfd20932" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-script-runtime-1.9.10.pom"> + <sha256 value="5748be5895ed7dc30154e892f8e15b0236599e89e500c2da0bb6549e3385d2c7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-script-runtime" version="2.0.0"> + <artifact name="kotlin-script-runtime-2.0.0.jar"> + <sha256 value="b57a14387a504f1d3d8634899a830cb0bc5fe1adb86a62cc65d940fe982b04f9" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-script-runtime-2.0.0.pom"> + <sha256 value="e7540d47f555359526927b747aa47323f9897b94f72eb8ce451e36de752533db" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-scripting-common" version="2.0.0"> + <artifact name="kotlin-scripting-common-2.0.0.jar"> + <sha256 value="3cb9f18af2409c90793dfc6ce265a1a9268df4c7e5de3e2a2bcee7ec86059179" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-scripting-common-2.0.0.pom"> + <sha256 value="97283d9b9908d89039cd6e40df6dd810e9aa144c4320eae6b6310a8b6eecee50" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-scripting-compiler-embeddable" version="2.0.0"> + <artifact name="kotlin-scripting-compiler-embeddable-2.0.0.jar"> + <sha256 value="1f914b74e4e135d52478d63210570f90dab04cc6a6f7d54e1f3ea8cf57a1a754" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-scripting-compiler-embeddable-2.0.0.pom"> + <sha256 value="072ff8c68f0a522837fa2c667071fbf7a7b3d9d6d3bc3b58d5a92f20d0af7c9b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-scripting-compiler-impl-embeddable" version="2.0.0"> + <artifact name="kotlin-scripting-compiler-impl-embeddable-2.0.0.jar"> + <sha256 value="98de5f7838914e6880ecaef3a6e08e5cb165f16629afa0a2df3550cf7f22d2bf" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-scripting-compiler-impl-embeddable-2.0.0.pom"> + <sha256 value="5bf861970ba3f7320df97e4e423727ae405aee331adf86486bbeb96eed908dc8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-scripting-jvm" version="2.0.0"> + <artifact name="kotlin-scripting-jvm-2.0.0.jar"> + <sha256 value="7f20010fba093904eed7a8b84938bf9934be4747ef25f676d17bc301bd139a62" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-scripting-jvm-2.0.0.pom"> + <sha256 value="0499bd736d62273065604f65b1632d5a0f2ff1b0309f079f0840f7f6e17ca107" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.7.10"> + <artifact name="kotlin-stdlib-1.7.10.pom"> + <sha256 value="6cc0cf5a2bc02dee060ebb90c3535fc3ddbd7a3bab210ace3e142aaf81764d81" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.8.22"> + <artifact name="kotlin-stdlib-1.8.22.jar"> + <sha256 value="03a5c3965cc37051128e64e46748e394b6bd4c97fa81c6de6fc72bfd44e3421b" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-1.8.22.pom"> + <sha256 value="596f4a62f3086ad98f0e78b3e257ba5b247cd313bffa9818e509696210e83a37" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.9.10"> + <artifact name="kotlin-stdlib-1.9.10.jar"> + <sha256 value="55e989c512b80907799f854309f3bc7782c5b3d13932442d0379d5c472711504" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-1.9.10.pom"> + <sha256 value="7e29fbf73fdf71e0679d3dee7e680fd573464fa01644a4f58ab819d2c088d3d2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.9.20"> + <artifact name="kotlin-stdlib-1.9.20.jar"> + <sha256 value="28a35bcdff46d864f80f346a617e486284b208d17378c41900dfb1de95a90e6c" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-1.9.20.module"> + <sha256 value="dccaa5d315470fab3920502886bbb85f2da6c86102c65d9c04410544eedb2019" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.9.21"> + <artifact name="kotlin-stdlib-1.9.21.jar"> + <sha256 value="3b479313ab6caea4e5e25d3dee8ca80c302c89ba73e1af4dafaa100f6ef9296a" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-1.9.21.module"> + <sha256 value="d3019f7f0d71924ce47298c9cc46af0245f75219719b35c5915fbcc7e7a69395" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.9.22"> + <artifact name="kotlin-stdlib-1.9.22-all.jar"> + <sha256 value="cec38bc3302e72a8aaf9cde436b5a9071ee0331e2ad05e84d8bb897334d7e9d4" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-1.9.22.module"> + <sha256 value="f482314b5079c1455f6fb0d4257a745d101c6124ce961522ba86f9dc90901e47" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="1.9.23"> + <artifact name="kotlin-stdlib-1.9.23.jar"> + <sha256 value="8910cc238807d86ef550cb1f0b10dd5ed40b35a4ec1a52525f760aede84ead37" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-1.9.23.module"> + <sha256 value="5195193b37dcdada2e1c0ab0d512c422b2ad76af3557843a1b9c3480f4e71d0e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib" version="2.0.0"> + <artifact name="kotlin-stdlib-2.0.0-all.jar"> + <sha256 value="30e05222bc067fffb896a3276b5f1e3f29450bf1d9461d27a19ce0efc793b5cd" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-2.0.0-sources.jar"> + <sha256 value="256f2c1caf3df558d6c41b79fb83add98edcc08436b821a80b5f17d806c664a1" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-2.0.0.jar"> + <sha256 value="240938c4aab8e73e888703e3e7d3f87383ffe5bd536d6d5e3c100d4cd0379fcf" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-2.0.0.module"> + <sha256 value="64f96ea8e7b9896731052241ffd3a265f8274d761e5fe9dc088ac45b31718341" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.7.10"> + <artifact name="kotlin-stdlib-common-1.7.10.pom"> + <sha256 value="1011c63b88ee94cdff5d596937307559bc55037b733cc00ce63cda3cfae0a8eb" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.8.22"> + <artifact name="kotlin-stdlib-common-1.8.22.jar"> + <sha256 value="d0c2365e2437ef70f34586d50f055743f79716bcfe65e4bc7239cdd2669ef7c5" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-common-1.8.22.pom"> + <sha256 value="a72b11df08b5322d7a5e8e620789ee3e4cfef38e86c430e7d113bfa9e54c581e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.9.10"> + <artifact name="kotlin-stdlib-common-1.9.10.jar"> + <sha256 value="cde3341ba18a2ba262b0b7cf6c55b20c90e8d434e42c9a13e6a3f770db965a88" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-common-1.9.10.pom"> + <sha256 value="7d4b70547910676b3bdfc8925a88f3b6bfb24582c9784542805544ceef490a92" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.9.20"> + <artifact name="kotlin-stdlib-common-1.9.20.module"> + <sha256 value="858828bc5191b9e602affa14e01d66489dafb08c4c18d2faee3cbed7ba7d9992" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.9.21"> + <artifact name="kotlin-stdlib-common-1.9.21.module"> + <sha256 value="68dc8e84aa05f5278c16505247d71346d3512b9edadd41f613d657c94c59993b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="1.9.22"> + <artifact name="kotlin-stdlib-common-1.9.22.module"> + <sha256 value="f93c9e9abf8d52d8e8fd8e851aa802ecec55132161c4aeee7d3cd924bf794246" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-common" version="2.0.0"> + <artifact name="kotlin-stdlib-common-2.0.0.module"> + <sha256 value="2335187440c51d0d69e1b906fefc31f6169691c8598177b0e610c9b9a92ce6b5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk7" version="1.8.21"> + <artifact name="kotlin-stdlib-jdk7-1.8.21.jar"> + <sha256 value="33d148db0e11debd0d90677d28242bced907f9c77730000fd597867089039d86" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-jdk7-1.8.21.pom"> + <sha256 value="9bb107d5d5e3930bc5977f007a43cd20b7d24d91b1c1e528ea6ee0f248f14d36" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk7" version="1.8.22"> + <artifact name="kotlin-stdlib-jdk7-1.8.22.jar"> + <sha256 value="055f5cb24287fa106100995a7b47ab92126b81e832e875f5fa2cf0bd55693d0b" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-jdk7-1.8.22.pom"> + <sha256 value="4f958aa993d5984f8f5ebed4146562a5f3a9f695b6049c9f287381379cadab33" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk7" version="1.9.0"> + <artifact name="kotlin-stdlib-jdk7-1.9.0.jar"> + <sha256 value="b7979a7aac94055f0d9f1fd3b47ce5ffe1cb6032a842ba9fbe7186f085289178" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-jdk7-1.9.0.pom"> + <sha256 value="c11074f0c898a98b863c614471d438d3df92a1ec3382a6e37f935d7d71954b5a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk7" version="1.9.10"> + <artifact name="kotlin-stdlib-jdk7-1.9.10.jar"> + <sha256 value="ac6361bf9ad1ed382c2103d9712c47cdec166232b4903ed596e8876b0681c9b7" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-jdk7-1.9.10.pom"> + <sha256 value="c7fa67c7961320b89d85a3ca59a2e18c2c65850845595dcae4b46af6945edcd5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk7" version="1.9.20"> + <artifact name="kotlin-stdlib-jdk7-1.9.20.jar"> + <sha256 value="c5451d67a27f33afd09913c67e1ceba3897ae70884b24ef0ff71157e55b60865" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-jdk7-1.9.20.pom"> + <sha256 value="012e1c55ed6ade417bcb824112ebea682ad625dfbee16085c379ccfa14faf033" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk7" version="2.0.0"> + <artifact name="kotlin-stdlib-jdk7-2.0.0.jar"> + <sha256 value="dd958064f946b01f745365c2efa11e4c5812a22e26b6caa4433f207fb5b4a5c5" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-jdk7-2.0.0.pom"> + <sha256 value="1ea14bf50d37b24cee99b3bbeca0e24a07ff361d906bb20067cf41606e143096" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk8" version="1.8.21"> + <artifact name="kotlin-stdlib-jdk8-1.8.21.jar"> + <sha256 value="3db752a30074f06ee6c57984aa6f27da44f4d2bbc7f5442651f6988f1cb2b7d7" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-jdk8-1.8.21.pom"> + <sha256 value="3839d728d7c309a5c368b0270b44b4f1c7878ff5ca5d32a9a204faa3491459d8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk8" version="1.8.22"> + <artifact name="kotlin-stdlib-jdk8-1.8.22.jar"> + <sha256 value="4198b0eaf090a4f25b6f7e5a59581f4314ba8c9f6cd1d13ee9d348e65ed8f707" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-jdk8-1.8.22.pom"> + <sha256 value="928f2187217476313cb816d48070bc7654aa3b9a5ae81d3fc5f8c279cc8f8d9e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk8" version="1.9.0"> + <artifact name="kotlin-stdlib-jdk8-1.9.0.jar"> + <sha256 value="a59fa24fdf1ffb594baecdbf0fd10010f977cea10236d487fe3464977a7377fa" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-jdk8-1.9.0.pom"> + <sha256 value="64d598dd88e250466731e20304ab6f06cbbbbab7ee322b4703b6b59f881c4f92" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk8" version="1.9.10"> + <artifact name="kotlin-stdlib-jdk8-1.9.10.jar"> + <sha256 value="a4c74d94d64ce1abe53760fe0389dd941f6fc558d0dab35e47c085a11ec80f28" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-jdk8-1.9.10.pom"> + <sha256 value="5f4b94dd3065a7764c37fa15de2ad6d81f40d59f8cb33f17d181c6384fb7a72e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk8" version="1.9.20"> + <artifact name="kotlin-stdlib-jdk8-1.9.20.jar"> + <sha256 value="f833fcc94f0bb1c31b9e78bd4bda7ea23f579ff3408ae1a94e2eb5747086a2ab" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-jdk8-1.9.20.pom"> + <sha256 value="a3b07deb091f2aed59d6559884f44f99cff5df9b9fa35a24bcd6b8b069cff48d" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-stdlib-jdk8" version="2.0.0"> + <artifact name="kotlin-stdlib-jdk8-2.0.0.jar"> + <sha256 value="92035a0fa8b97a97bb430d30ba03e5ad51f4cfa839cf1559e55245cba735aa94" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-stdlib-jdk8-2.0.0.pom"> + <sha256 value="afe5006f9d2bb7a26a1f1b92b007a871be26dd82fadbbb2cca9f3757534c5b83" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-tooling-core" version="2.0.0"> + <artifact name="kotlin-tooling-core-2.0.0.jar"> + <sha256 value="73464aa3c214567013a4373f09e7b6f0b63185b3a6df6988b347430d7777a971" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-tooling-core-2.0.0.pom"> + <sha256 value="cf6255cbe64be5cfad16dd9a037e504264496c57c4f755e2f3527ab45aa166ba" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-util-io" version="2.0.0"> + <artifact name="kotlin-util-io-2.0.0.jar"> + <sha256 value="088e45d9522db6406f0b1e4ed328389c9b5992a9567cd91b57ae21c1b9221610" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-util-io-2.0.0.pom"> + <sha256 value="c2cf75fd427eb63b7d321f4f10914675fe63320a90595f465a82cc5a1b64f4f7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin" name="kotlin-util-klib" version="2.0.0"> + <artifact name="kotlin-util-klib-2.0.0.jar"> + <sha256 value="95f1a677e8d24bc0b32e76783ef173c5c0e09ed41b50471736d6599bed19d875" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlin-util-klib-2.0.0.pom"> + <sha256 value="0b9321f812048eecd0391765c882188fb7ea0f30ea84f8cf6874ab58371ee21f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin.android" name="org.jetbrains.kotlin.android.gradle.plugin" version="2.0.0"> + <artifact name="org.jetbrains.kotlin.android.gradle.plugin-2.0.0.pom"> + <sha256 value="82549d03a6f5dbd67afb0901d8b77cd402ec3ab9d6223a91de604beadddce1f8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlin.plugin.parcelize" name="org.jetbrains.kotlin.plugin.parcelize.gradle.plugin" version="2.0.0"> + <artifact name="org.jetbrains.kotlin.plugin.parcelize.gradle.plugin-2.0.0.pom"> + <sha256 value="c96cb56fc41ad5298cf93117ab438999bb4d661e3e596ee9d4eafb9017c84150" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="atomicfu" version="0.23.1"> + <artifact name="atomicfu-0.23.1.module"> + <sha256 value="3e891fe636b55108192100fcf38b1a39bcd1c2533e23c462fc07644eeafcb20f" origin="Generated by Gradle"/> + </artifact> + <artifact name="atomicfu-metadata-0.23.1.jar"> + <sha256 value="7db8660ebe4b91bb478edb3616c4e3a50ba59c07dca517d1e1284c03fe86ac57" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-android" version="1.7.1"> + <artifact name="kotlinx-coroutines-android-1.7.1.jar"> + <sha256 value="107313760c18f8da174e8d8103504a468e806e88f7b55a84bd1c0eaeea118e9a" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinx-coroutines-android-1.7.1.module"> + <sha256 value="beb7ff0f5ebc63a0b30af2ae1214e0b622a7b7e408240e64a8ea5b213c4d5334" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-android" version="1.8.1"> + <artifact name="kotlinx-coroutines-android-1.8.1.jar"> + <sha256 value="a134dacf4e6578b29b32e97aa50548d09cb45e3cb3551ce77ac27e55e265d8f5" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinx-coroutines-android-1.8.1.module"> + <sha256 value="f00af0920cb7dc4611205ff592eec5aa0071bc959b3797026abfe43cf14bfad4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-bom" version="1.6.4"> + <artifact name="kotlinx-coroutines-bom-1.6.4.pom"> + <sha256 value="ab2614855fba66aa8a42514dbe3d5a884315ffe1ed63f5932e710a8006245ce1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-bom" version="1.7.1"> + <artifact name="kotlinx-coroutines-bom-1.7.1.pom"> + <sha256 value="b925aa988c40a5c7aa0c77b21373feb18c39414926bd51c76ef44434f5bae9c2" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-bom" version="1.8.0"> + <artifact name="kotlinx-coroutines-bom-1.8.0.pom"> + <sha256 value="1239e9dbe1397cd5971342956b2511bc3ace7b641842e4372a088dcfa8b9ad55" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-bom" version="1.8.1"> + <artifact name="kotlinx-coroutines-bom-1.8.1.pom"> + <sha256 value="563e4aa29fa8fe09a6e1746d0a5b51308f7c7dedc468dc5a0ca810a485877030" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.6.4"> + <artifact name="kotlinx-coroutines-core-1.6.4.module"> + <sha256 value="a6eed4a1835588e7c84fcd7b0475fce9a7b3444c870ebc797b88ba64ccf4576b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core" version="1.8.1"> + <artifact name="kotlinx-coroutines-core-1.8.1.module"> + <sha256 value="08cbaf3325b54e0f8ef8da85e4eb596f7d946f843e5d1600385463f3268a4ef0" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinx-coroutines-core-metadata-1.8.1.jar"> + <sha256 value="daf50f1c9404b224a1d6dd5286f8e8ee7d63fe807f78ea98f71795c183b6025f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.5.0"> + <artifact name="kotlinx-coroutines-core-jvm-1.5.0.jar"> + <sha256 value="78d6cc7135f84d692ff3752fcfd1fa1bbe0940d7df70652e4f1eaeec0c78afbb" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinx-coroutines-core-jvm-1.5.0.module"> + <sha256 value="c885dd0281076c5843826de317e3cbcdc3d8859dbeef53ae1cfacd1b9c60f96e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.6.4"> + <artifact name="kotlinx-coroutines-core-jvm-1.6.4.jar"> + <sha256 value="c24c8bb27bb320c4a93871501a7e5e0c61607638907b197aef675513d4c820be" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinx-coroutines-core-jvm-1.6.4.module"> + <sha256 value="0d94c8a41483e7c2707ebd693e1b1357a84152998ce85550ebbc54ca4321a3a7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.8.0"> + <artifact name="kotlinx-coroutines-core-jvm-1.8.0.module"> + <sha256 value="ff6a22da40040938751db0ae21177e76517dbf126a76796f5426727bf76c1228" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-core-jvm" version="1.8.1"> + <artifact name="kotlinx-coroutines-core-jvm-1.8.1.jar"> + <sha256 value="f3d4f5de1c391bbcc20f3b3435ccbac013521e76b6902d7d59635ec15c1f797e" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinx-coroutines-core-jvm-1.8.1.module"> + <sha256 value="09b81c9d11c2deebf133ad87b707927c1f04099cb611ef008c7725b3eb308329" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-test" version="1.8.1"> + <artifact name="kotlinx-coroutines-test-1.8.1.module"> + <sha256 value="a1cee2dab296c13b78ec1c060e18fe40334a440c8a077e90cca6de72527d8cde" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinx-coroutines-test-metadata-1.8.1.jar"> + <sha256 value="d5a9b8769a2167eb9dc911d1b2c54fe8270387b8160533d8ae8bda942e2998b1" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-coroutines-test-jvm" version="1.8.1"> + <artifact name="kotlinx-coroutines-test-jvm-1.8.1-sources.jar"> + <sha256 value="bea89f1396fcb311f672d6f82ea6504c5bf60b426d97611473ad04ffaf4253ba" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinx-coroutines-test-jvm-1.8.1.jar"> + <sha256 value="c4ef1deb31be3f81ed82ecf237220cc95886868a7ec527a418599dfff159dedb" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinx-coroutines-test-jvm-1.8.1.module"> + <sha256 value="fb08fc257c900433d2df997bd6c29e049cd9f7bf541c0b776180e0998241f576" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-serialization-core" version="1.4.1"> + <artifact name="kotlinx-serialization-core-1.4.1.module"> + <sha256 value="60e581c397ddb9461ec1f1dee5bbb4a23bb7ec7d09b296024196220022aa7090" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-serialization-core-jvm" version="1.4.1"> + <artifact name="kotlinx-serialization-core-jvm-1.4.1.jar"> + <sha256 value="eba7f1c854296e4ce1418fb01360f8f10c5683e7c45aa3472018417a067636f3" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinx-serialization-core-jvm-1.4.1.module"> + <sha256 value="73bc94bdd5fc86621509a6715c3fe344904ee7db5806a0c61792ce2356089ee9" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-serialization-json" version="1.4.1"> + <artifact name="kotlinx-serialization-json-1.4.1.module"> + <sha256 value="e9922300aff663e55ecef7d3fca305cb60a14755b1f980d94039e3c1cab645cc" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jetbrains.kotlinx" name="kotlinx-serialization-json-jvm" version="1.4.1"> + <artifact name="kotlinx-serialization-json-jvm-1.4.1.jar"> + <sha256 value="af604c46737121d4225fdb60ef0e17766a3c94b7c1c9ef76b4e3a5c7733d557e" origin="Generated by Gradle"/> + </artifact> + <artifact name="kotlinx-serialization-json-jvm-1.4.1.module"> + <sha256 value="c8fbfde4b5ee1e41a69175165e839991d1501665a7590e23162326501ac6122c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jlleitschuh.gradle" name="ktlint-gradle" version="12.1.1"> + <artifact name="ktlint-gradle-12.1.1.jar"> + <sha256 value="f314e27ae92dda118e4f0b5eb9f2d867286ed0ccb72090a0a53b5c9f15c032ef" origin="Generated by Gradle"/> + </artifact> + <artifact name="ktlint-gradle-12.1.1.module"> + <sha256 value="748378699d6028dab55c6cf7e4cc663aa992e9dd2c90c19a3a92dabaf1700d8a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jlleitschuh.gradle.ktlint" name="org.jlleitschuh.gradle.ktlint.gradle.plugin" version="12.1.1"> + <artifact name="org.jlleitschuh.gradle.ktlint.gradle.plugin-12.1.1.pom"> + <sha256 value="5a99bee1b84a692595f2df151b5055305ec94a978959612f7b94ada3fb0aa6d8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.junit" name="junit-bom" version="5.9.2"> + <artifact name="junit-bom-5.9.2.module"> + <sha256 value="ab137ba5a8e32c9b066bf9126a1c76dd5614b724ba5c0b02549772b5e9f4cf1f" origin="Generated by Gradle"/> + </artifact> + <artifact name="junit-bom-5.9.2.pom"> + <sha256 value="2ed07d65845131f5336a86476c9a4056b59d0b58b9815ab3679bb0f36f35f705" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.junit" name="junit-bom" version="5.9.3"> + <artifact name="junit-bom-5.9.3.module"> + <sha256 value="b401fd25901e582a524aa5343c4b39e28bc56e24961c1069bf2b4bbfcee46b93" origin="Generated by Gradle"/> + </artifact> + <artifact name="junit-bom-5.9.3.pom"> + <sha256 value="4d0329cd9e72f2420e5ca15724cbfe6ffa6e5fd2888361516271190fdc342ed7" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.jvnet.staxex" name="stax-ex" version="1.8.1"> + <artifact name="stax-ex-1.8.1.jar"> + <sha256 value="20522549056e9e50aa35ef0b445a2e47a53d06be0b0a9467d704e2483ffb049a" origin="Generated by Gradle"/> + </artifact> + <artifact name="stax-ex-1.8.1.pom"> + <sha256 value="8fc84f36ce6da6ce8c893b6538199a7f69a69a0706d9b17a3ee6a3a09452eed6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.mockito" name="mockito-core" version="5.7.0"> + <artifact name="mockito-core-5.7.0-sources.jar"> + <sha256 value="a0a2b963d94a9dbb7057bdf421a4677d578a1db55b7c7e9f94ac44f0780aff3f" origin="Generated by Gradle"/> + </artifact> + <artifact name="mockito-core-5.7.0.jar"> + <sha256 value="dbad5e746654910a11a59ecb4d01e38461f3e5d16161689dc2588d5554432521" origin="Generated by Gradle"/> + </artifact> + <artifact name="mockito-core-5.7.0.pom"> + <sha256 value="79e673a535f48ca37eebf76cb7ac4c699d4946ceed6f082b040c5936d44300ef" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.mockito" name="mockito-inline" version="5.2.0"> + <artifact name="mockito-inline-5.2.0-sources.jar"> + <sha256 value="ee52e1c299a632184fba274a9370993e09140429f5e516e6c5570fd6574b297f" origin="Generated by Gradle"/> + </artifact> + <artifact name="mockito-inline-5.2.0.jar"> + <sha256 value="ee52e1c299a632184fba274a9370993e09140429f5e516e6c5570fd6574b297f" origin="Generated by Gradle"/> + </artifact> + <artifact name="mockito-inline-5.2.0.pom"> + <sha256 value="706d3470e56d31a3b5630698d1079befdb983145b0184e4ba2b84da387a8f684" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.mockito.kotlin" name="mockito-kotlin" version="5.3.1"> + <artifact name="mockito-kotlin-5.3.1-sources.jar"> + <sha256 value="52fa63b4f95999a631abb196d3195a7c10d42762690a5e5498f9c8527629ce84" origin="Generated by Gradle"/> + </artifact> + <artifact name="mockito-kotlin-5.3.1.jar"> + <sha256 value="e52ad013a6e64d352741835346129f54f7235db1aacb5030f296c4c6ec19b899" origin="Generated by Gradle"/> + </artifact> + <artifact name="mockito-kotlin-5.3.1.pom"> + <sha256 value="5f6b44674872eb8fd27a84ff421af3e0f965c4874367a1eef2f053ccfadd4dbf" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.objenesis" name="objenesis" version="3.3"> + <artifact name="objenesis-3.3-sources.jar"> + <sha256 value="d06164f8ca002c8ef193cef2d682822014dd330505616af93a3fb64226fc131d" origin="Generated by Gradle"/> + </artifact> + <artifact name="objenesis-3.3.jar"> + <sha256 value="02dfd0b0439a5591e35b708ed2f5474eb0948f53abf74637e959b8e4ef69bfeb" origin="Generated by Gradle"/> + </artifact> + <artifact name="objenesis-3.3.pom"> + <sha256 value="ba0c40da2669a048b6e24ef7066a471f0fbcbfcc509e6a3e856ca4ddfa614ad3" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.objenesis" name="objenesis-parent" version="3.3"> + <artifact name="objenesis-parent-3.3.pom"> + <sha256 value="305c384aa2f1e1c7fe53a96da41c3ec35243b97d428d24a8f779818cc10be4ff" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.ow2" name="ow2" version="1.5"> + <artifact name="ow2-1.5.pom"> + <sha256 value="0f8a1b116e760b8fe6389c51b84e4b07a70fc11082d4f936e453b583dd50b43b" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.ow2" name="ow2" version="1.5.1"> + <artifact name="ow2-1.5.1.pom"> + <sha256 value="321ddbb7ee6fe4f53dea6b4cd6db74154d6bfa42391c1f763b361b9f485acf05" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.ow2.asm" name="asm" version="9.3"> + <artifact name="asm-9.3.jar"> + <sha256 value="1263369b59e29c943918de11d6d6152e2ec6085ce63e5710516f8c67d368e4bc" origin="Generated by Gradle"/> + </artifact> + <artifact name="asm-9.3.pom"> + <sha256 value="8eac07e29f8aea83a8156d7b29fa368f6ebf3becfb209c9a1ac36abd906123e0" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.ow2.asm" name="asm" version="9.6"> + <artifact name="asm-9.6-sources.jar"> + <sha256 value="2b6e12f0da3d065ba628a024a8851ab0d5b5d3501dacfcc18769243250f4f77e" origin="Generated by Gradle"/> + </artifact> + <artifact name="asm-9.6.jar"> + <sha256 value="3c6fac2424db3d4a853b669f4e3d1d9c3c552235e19a319673f887083c2303a1" origin="Generated by Gradle"/> + </artifact> + <artifact name="asm-9.6.pom"> + <sha256 value="92eee24bc3c843e4881d46c1dd6505471ee3142facfb466b428cfea5a56c6b60" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.ow2.asm" name="asm" version="9.7"> + <artifact name="asm-9.7.jar"> + <sha256 value="adf46d5e34940bdf148ecdd26a9ee8eea94496a72034ff7141066b3eea5c4e9d" origin="Generated by Gradle"/> + </artifact> + <artifact name="asm-9.7.pom"> + <sha256 value="de00115f1d84f3a0b2ee3a4b6f6192d066f86d185d67b9d1522f2c80feac5f00" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.ow2.asm" name="asm-analysis" version="9.6"> + <artifact name="asm-analysis-9.6.jar"> + <sha256 value="d92832d7c37edc07c60e2559ac6118b31d642e337a6671edcb7ba9fae68edbbb" origin="Generated by Gradle"/> + </artifact> + <artifact name="asm-analysis-9.6.pom"> + <sha256 value="fa3f995021cff4f4139306e6cffeeea07539106440d29b19cc06a7a636a13850" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.ow2.asm" name="asm-analysis" version="9.7"> + <artifact name="asm-analysis-9.7.jar"> + <sha256 value="7bc6bcbc21379948a0c8c467fb0f864206e5b818f6bc0b546872f5c9f941556f" origin="Generated by Gradle"/> + </artifact> + <artifact name="asm-analysis-9.7.pom"> + <sha256 value="9c33080ebcb631ae4f77eb62ed67bfc40cb872e8cfd058ac863e445c1dd973df" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.ow2.asm" name="asm-commons" version="9.6"> + <artifact name="asm-commons-9.6.jar"> + <sha256 value="7aefd0d5c0901701c69f7513feda765fb6be33af2ce7aa17c5781fc87657c511" origin="Generated by Gradle"/> + </artifact> + <artifact name="asm-commons-9.6.pom"> + <sha256 value="a98ae4895334baf8ff86bd66516210dbd9a03f1a6e15e47dda82afcf6b53d77c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.ow2.asm" name="asm-commons" version="9.7"> + <artifact name="asm-commons-9.7.jar"> + <sha256 value="389bc247958e049fc9a0408d398c92c6d370c18035120395d4cba1d9d9304b7a" origin="Generated by Gradle"/> + </artifact> + <artifact name="asm-commons-9.7.pom"> + <sha256 value="5acee3ee7252ed90b8074c755d022787499a95fafff98ac4a685107c4da409b4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.ow2.asm" name="asm-tree" version="9.6"> + <artifact name="asm-tree-9.6.jar"> + <sha256 value="c43ecf17b539c777e15da7b5b86553b377e2d39a683de6285567d5283888e7ef" origin="Generated by Gradle"/> + </artifact> + <artifact name="asm-tree-9.6.pom"> + <sha256 value="1bcb481d7fc16b955bb60ca07c8cfa2424bcee78bdc405bba31c7d6f5dc2d113" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.ow2.asm" name="asm-tree" version="9.7"> + <artifact name="asm-tree-9.7.jar"> + <sha256 value="62f4b3bc436045c1acb5c3ba2d8ec556ec3369093d7f5d06c747eb04b56d52b1" origin="Generated by Gradle"/> + </artifact> + <artifact name="asm-tree-9.7.pom"> + <sha256 value="a34ea1e3e4128c01038db43c6976e88c779cf5af84b0505da266dfe6965668ec" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.ow2.asm" name="asm-util" version="9.6"> + <artifact name="asm-util-9.6.jar"> + <sha256 value="c635a7402f4aa9bf66b2f4230cea62025a0fe1cd63e8729adefc9b1994fac4c3" origin="Generated by Gradle"/> + </artifact> + <artifact name="asm-util-9.6.pom"> + <sha256 value="52c5c1d357404779d1a99b49a85bf9d3a085025b8516cb73dbef77c8ae34005e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.ow2.asm" name="asm-util" version="9.7"> + <artifact name="asm-util-9.7.jar"> + <sha256 value="37a6414d36641973f1af104937c95d6d921b2ddb4d612c66c5a9f2b13fc14211" origin="Generated by Gradle"/> + </artifact> + <artifact name="asm-util-9.7.pom"> + <sha256 value="5d014d8c870d4871825bd2ddb5567b21ef6dac8ec48bbb8dbb465b0b3a2bf452" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.pageseeder.diffx" name="pso-diffx" version="1.1.1"> + <artifact name="pso-diffx-1.1.1.jar"> + <sha256 value="c539842b0459fe625062a5ef46cc4449c59f553110cfbdc8c99cc9903bec9690" origin="Generated by Gradle"/> + </artifact> + <artifact name="pso-diffx-1.1.1.module"> + <sha256 value="810b32c25f20b1edaa19289f819936741564a9cdf18e52593fc0e20252b3fbac" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.pageseeder.xmlwriter" name="pso-xmlwriter" version="1.0.4"> + <artifact name="pso-xmlwriter-1.0.4.jar"> + <sha256 value="3d5c89f22fd7b8097665a773574679aedf0ca5bc605cd3b2aed17ccba9e47e3e" origin="Generated by Gradle"/> + </artifact> + <artifact name="pso-xmlwriter-1.0.4.module"> + <sha256 value="8fbaab52db349a1380d366532d2f5167d3d78fc47ecc416cef22ae3d4655f067" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.robolectric" name="annotations" version="4.12.2"> + <artifact name="annotations-4.12.2.jar"> + <sha256 value="329ce893a1b3e96c06ee75d07b9b9f99ecf6389acb5c0d17fcb9d25350db0a7a" origin="Generated by Gradle"/> + </artifact> + <artifact name="annotations-4.12.2.module"> + <sha256 value="48fc50360373e13da0596f0cea62b0f3b4e8288c20571194b6c775f5d36e0aa5" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.robolectric" name="junit" version="4.12.2"> + <artifact name="junit-4.12.2.jar"> + <sha256 value="f3f6ebf476b4ecdffcfe4e8b3b3d6119cbe2fc59f19822d8a6c7df35dbf22c48" origin="Generated by Gradle"/> + </artifact> + <artifact name="junit-4.12.2.module"> + <sha256 value="1b0e41d67cf4023014503af4fbf73b24f67ddc47ddd45744d3f99993be9e4847" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.robolectric" name="nativeruntime" version="4.12.2"> + <artifact name="nativeruntime-4.12.2.jar"> + <sha256 value="f8e55ab954404735f5931b5179e139e158367139b7deeddf6ded7086a0467d9d" origin="Generated by Gradle"/> + </artifact> + <artifact name="nativeruntime-4.12.2.module"> + <sha256 value="bccb3547dccc7dc990d60ed3b9a771982a008aab31c8a3021e8508614c0e95b6" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.robolectric" name="nativeruntime-dist-compat" version="1.0.10"> + <artifact name="nativeruntime-dist-compat-1.0.10.jar"> + <sha256 value="1159b8394b9d9640b53b63f07ec1fe676a7b394a90b8361eef2a88868cca3f3d" origin="Generated by Gradle"/> + </artifact> + <artifact name="nativeruntime-dist-compat-1.0.10.pom"> + <sha256 value="de771e6ce96737fc1d50713b2b5f28f0624d65b8ba323526775bb3b7ca07575e" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.robolectric" name="pluginapi" version="4.12.2"> + <artifact name="pluginapi-4.12.2.jar"> + <sha256 value="822af3ccba184421c36684ec4678e1b1b4424520891d46b1f568c10d3b036ba5" origin="Generated by Gradle"/> + </artifact> + <artifact name="pluginapi-4.12.2.module"> + <sha256 value="4328d5d9072cd1cbeb447bcf7eb13969e29bd9a2350387ba4fca4a976849a99c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.robolectric" name="plugins-maven-dependency-resolver" version="4.12.2"> + <artifact name="plugins-maven-dependency-resolver-4.12.2.jar"> + <sha256 value="af77ec038d8c9771fed6942fc905520c80231c1aef321ede2869227e9bd8fb08" origin="Generated by Gradle"/> + </artifact> + <artifact name="plugins-maven-dependency-resolver-4.12.2.pom"> + <sha256 value="9f33e449083d5d4d349725c34f2c8bd7f4cd7a388f2678575a8644c3f1789d17" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.robolectric" name="resources" version="4.12.2"> + <artifact name="resources-4.12.2.jar"> + <sha256 value="7a3371613ac23070030566ac590297b301362aba8cba9372acf0f3701cfe5c99" origin="Generated by Gradle"/> + </artifact> + <artifact name="resources-4.12.2.module"> + <sha256 value="76f9990411816bb6ccbccb4840d308415cc7c46319f9dad19ee488c207f0e633" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.robolectric" name="robolectric" version="4.12.2"> + <artifact name="robolectric-4.12.2.jar"> + <sha256 value="b8eb9d7cf85bb9636f430d68a50768d1a01a747c94b40b8e8a79abe6a3f95f82" origin="Generated by Gradle"/> + </artifact> + <artifact name="robolectric-4.12.2.module"> + <sha256 value="66c2cdcd83b4ddf4bddd8894d5515e9a5eb27536d39c4b9acf9ae121abbd94a4" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.robolectric" name="sandbox" version="4.12.2"> + <artifact name="sandbox-4.12.2.jar"> + <sha256 value="8f0c3966ab51d0fca1ede8a4e706440d3783dbf4f6ec5c484fd3fcf0d9b9c9bf" origin="Generated by Gradle"/> + </artifact> + <artifact name="sandbox-4.12.2.module"> + <sha256 value="9c4ff684965cc1e6d0249b336caf70b3edba93b71743feaa17f347494b520d9c" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.robolectric" name="shadowapi" version="4.12.2"> + <artifact name="shadowapi-4.12.2.jar"> + <sha256 value="1918a7cfd69f82e2f125a56fa032e3828437a941ec9f077e7a3a652e84cb5cdc" origin="Generated by Gradle"/> + </artifact> + <artifact name="shadowapi-4.12.2.module"> + <sha256 value="d7188d54f1eb0f2569f60a90734f7531670c43e57722e92eebdf41f4094cb940" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.robolectric" name="shadows-framework" version="4.12.2"> + <artifact name="shadows-framework-4.12.2.jar"> + <sha256 value="907dd8bb9966ba4b83a017822449fc8ab31ab0a437b29342cb2cfca1177354e1" origin="Generated by Gradle"/> + </artifact> + <artifact name="shadows-framework-4.12.2.module"> + <sha256 value="7c488a3496378b75e4053a190c8f65b4ed6fe5fb9ad459945425e25cab80ece8" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.robolectric" name="shadows-versioning" version="4.12.2"> + <artifact name="shadows-versioning-4.12.2.jar"> + <sha256 value="86126b5b074cb69c6953666ae467de1fa3b6d33bf794633d5b80a1be97ff4b1a" origin="Generated by Gradle"/> + </artifact> + <artifact name="shadows-versioning-4.12.2.module"> + <sha256 value="cb73240b5e1e037c00e1aba77c4ef24e5e29391a3c84a6adaf1018dc00d363ca" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.robolectric" name="utils" version="4.12.2"> + <artifact name="utils-4.12.2.jar"> + <sha256 value="80f49b35125981818d73b3a8df7324c08fe86a8ce4ca94b5fea324efecb034a9" origin="Generated by Gradle"/> + </artifact> + <artifact name="utils-4.12.2.pom"> + <sha256 value="19c600cd3c68401806fd728c6ab229c408a89ca2bafe2cd1e35443d3bd09ff53" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.robolectric" name="utils-reflector" version="4.12.2"> + <artifact name="utils-reflector-4.12.2.jar"> + <sha256 value="7e8a926d88d473a05af7cbef07941e03ddf676e987d70f40096193dba0cf5621" origin="Generated by Gradle"/> + </artifact> + <artifact name="utils-reflector-4.12.2.module"> + <sha256 value="5866557c1e7f48778368fd8f2d30be0c42807200c92837ffb41d8f7e8ab1c15f" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.slf4j" name="slf4j-api" version="1.7.30"> + <artifact name="slf4j-api-1.7.30.jar"> + <sha256 value="cdba07964d1bb40a0761485c6b1e8c2f8fd9eb1d19c53928ac0d7f9510105c57" origin="Generated by Gradle"/> + </artifact> + <artifact name="slf4j-api-1.7.30.pom"> + <sha256 value="7e0747751e9b67e19dcb5206f04ea22cc03d250c422426402eadd03513f2c314" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.slf4j" name="slf4j-api" version="2.0.4"> + <artifact name="slf4j-api-2.0.4.jar"> + <sha256 value="57fa874599ace9259286b99253d7a877afdd2db4b07a6827ac6c847ca5d601a2" origin="Generated by Gradle"/> + </artifact> + <artifact name="slf4j-api-2.0.4.pom"> + <sha256 value="85b1af0535665ce8f23a2b9f1f581cc1e7877bb1289304140827731216c78ffd" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.slf4j" name="slf4j-parent" version="1.7.30"> + <artifact name="slf4j-parent-1.7.30.pom"> + <sha256 value="11647956e48a0c5bfb3ac33f6da7e83f341002b6857efd335a505b687be34b75" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.slf4j" name="slf4j-parent" version="2.0.4"> + <artifact name="slf4j-parent-2.0.4.pom"> + <sha256 value="dc9dc87f53a8af721ee49dbb521bd06cda06134214fdad9400d872a75426cffd" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.sonatype.oss" name="oss-parent" version="4"> + <artifact name="oss-parent-4.pom"> + <sha256 value="c513995cf019d9213d4fda666589937b2bf1bea5c4cdd337e6170e80b18406ee" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.sonatype.oss" name="oss-parent" version="7"> + <artifact name="oss-parent-7.pom"> + <sha256 value="b51f8867c92b6a722499557fc3a1fdea77bdf9ef574722fe90ce436a29559454" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.sonatype.oss" name="oss-parent" version="9"> + <artifact name="oss-parent-9.pom"> + <sha256 value="fb40265f982548212ff82e362e59732b2187ec6f0d80182885c14ef1f982827a" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.tensorflow" name="tensorflow-lite-metadata" version="0.1.0-rc2"> + <artifact name="tensorflow-lite-metadata-0.1.0-rc2.jar"> + <sha256 value="2c2a264f842498c36d34d2a7b91342490d9a962862c85baac1acd54ec2fca6d9" origin="Generated by Gradle"/> + </artifact> + <artifact name="tensorflow-lite-metadata-0.1.0-rc2.pom"> + <sha256 value="8359ad51e0476c8e0df7188a43f16d49733c4a428fb45e99794b783f01b97520" origin="Generated by Gradle"/> + </artifact> + </component> + <component group="org.xerial" name="sqlite-jdbc" version="3.41.2.2"> + <artifact name="sqlite-jdbc-3.41.2.2.jar"> + <sha256 value="0cdab410947e04b6743df99cf1543267ddd107357d6f76948d145be590fd497d" origin="Generated by Gradle"/> + </artifact> + <artifact name="sqlite-jdbc-3.41.2.2.pom"> + <sha256 value="ae11a3b11dcbac1b6403689e3fd8d82b2ceea55e82dbfc4bb32aededf8ccac8e" origin="Generated by Gradle"/> + </artifact> + </component> + </components> +</verification-metadata> diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e644113 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..a441313 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..b740cf1 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..25da30d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..8df6733 --- /dev/null +++ b/renovate.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ], + "packageRules": [ + { + "groupName": "Kotlin", + "groupSlug": "kotlin", + "matchPackagePrefixes": [ + "com.google.devtools.ksp" + ], + "matchPackagePatterns": [ + "org.jetbrains.kotlin.*" + ] + } + ] +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..a1967f9 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,44 @@ +pluginManagement { + repositories { + google { + content { + includeGroupByRegex(".*google.*") + includeGroupByRegex(".*android.*") + } + } + gradlePluginPortal() + } +} + +plugins { + id 'com.gradle.develocity' version '3.17.5' + id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' +} + +def isCI = providers.environmentVariable("CI").present + +develocity { + buildScan { + termsOfUseUrl = "https://gradle.com/terms-of-service" + termsOfUseAgree = "yes" + publishing.onlyIf { isCI } + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google { + content { + includeGroupByRegex(".*google.*") + includeGroupByRegex(".*android.*") + } + } + mavenCentral() + maven { url 'https://jitpack.io' } + } +} + +enableFeaturePreview("STABLE_CONFIGURATION_CACHE") + +include ':app'